C 从入门到放弃?本文主要总结了在C 开发或review过程中常见易出错点做了归纳总结,希望借此能增进大家对C 的了解,减少编程出错,提升工作效率,也可以作为C 开发的避坑攻略。
空指针调用成员函数会crash??
当调用一个空指针所指向的类的成员函数时,大多数人的反应都是程序会crash。空指针并不指向任何有效的内存地址,所以在调用成员函数时会尝试访问一个不存在的内存地址,从而导致程序崩溃。
事实上有点出乎意料,先来看段代码:
代码语言:javascript复制class MyClass {
public:
static void Test_Func1() {
cout << "Handle Test_Func1!" << endl;
}
void Test_Func2() {
cout << "Handle Test_Func2!" << endl;
}
void Test_Func3() {
cout << "Handle Test_Func3! value:" << value << endl;
}
virtual void Test_Func4() {
cout << "Handle Test_Func4!" << endl;
}
int value = 0;
};
int main() {
MyClass* ptr = nullptr;
ptr->Test_Func1(); // ok, print Handle Test_Func1!
ptr->Test_Func2(); // ok, print Handle Test_Func2!
ptr->Test_Func3(); // crash
ptr->Test_Func4(); // crash return 0;
}
上面例子中,空指针对Test_Func1和Test_Func2的调用正常,对Test_Func3和Test_Func4的调用会crash。可能很多人反应都会crash,实际上并没有,这是为啥?
类的成员函数并不与具体对象绑定,所有的对象共用同一份成员函数体,当程序被编译后,成员函数的地址即已确定,这份共有的成员函数体之所以能够把不同对象的数据区分开来,靠的是隐式传递给成员函数的this指针,成员函数中对成员变量的访问都是转化成"this->数据成员"的方式。因此,从这一角度说,成员函数与普通函数一样,只是多了this指针。而类的静态成员函数只能访问静态成员变量,不能访问非静态成员变量,所以静态成员函数不需要this指针作为隐式参数。
因此,Test_Func1是静态成员函数,不需要this指针,所以即使ptr是空指针,也不影响对Test_Fun1的正常调用。Test_Fun2虽然需要传递隐式指针,但是函数体中并没有使用到这个隐式指针,所以ptr为空也不影响对Test_Fun2的正常调用。Test_Fun3就不一样了,因为函数中使用到了非静态的成员变量,对num的调用被转化成this->num,也就是ptr->num,而ptr是空指针,因此会crash。Test_Fun4是虚函数,有虚函数的类会有一个成员变量,即虚表指针,当调用虚函数时,会使用虚表指针,对虚表指针的使用也是通过隐式指针使用的,因此Test_Fun4的调用也会crash。
同理,以下std::shared_ptr的调用也是如此,日常开发需要注意,记得加上判空。
代码语言:javascript复制std::shared_ptr<UrlHandler> url_handler;
...
if(url_handler->IsUrlNeedHandle(data)) {
url_handler->HandleUrl(param);
}
字符串相关
字符串查找
对字符串进行处理是一个很常见的业务场景,其中字符串查找也是非常常见的,但是用的不好也是会存在各种坑。常见的字符串查找方法有:std::string::find、std::string::find_first_of、std::string::find_first_not_of、std::string::find_last_of,各位C Engineer都能熟练使用了吗?先来段代码瞧瞧:
代码语言:javascript复制bool IsBlacklistDllFromSrv(const std::string& dll_name) {
try {
std::string target_str = dll_name;
std::transform(target_str.begin(), target_str.end(), target_str.begin(), ::tolower);
if (dll_blacklist_from_srv.find(target_str) != std::string::npos) {
return true;
}
}
catch (...) {
}
return false;
}
上面这段代码,看下来没啥问题的样子。但是仔细看下来,就会发现字符串比对这里逻辑不够严谨,存在很大的漏洞。std::string::find只是用来在字符串中查找指定的子字符串,只要包含该子串就符合,如果dll_blacklist_from_srv = "abcd.dll;hhhh.dll;test.dll" 是这样的字符串,传入d.dll、hh.dll、dll;test.dll也会命中逻辑,明显是不太符合预期的。
这里顺带回顾下C std::string常见的字符串查找的方法:
std::string::find 用于在字符串中查找指定的子字符串。如果找到了子串,则返回子串的起始位置,否则返回std::string::npos。用于各种字符串操作,例如判断子字符串是否存在、获取子字符串的位置等。通过结合其他成员函数和算法,可以实现更复杂的字符串处理逻辑。
std::string::find_first_of 用于查找字符串中第一个与指定字符集合中的任意字符匹配的字符,并返回其位置。可用来检查字符串中是否包含指定的某些字符或者查找字符串中第一个出现的特定字符
std::string::find_first_not_of 用于查找字符串中第一个不与指定字符集合中的任何字符匹配的字符,并返回其位置。
std::string::find_last_of 用于查找字符串中最后一个与指定字符集合中的任意字符匹配的字符,并返回其位置。可以用来检查字符串中是否包含指定的某些字符,或者查找字符串中最后一个出现的特定字符
std::string::find_last_not_of 用于查找字符串中最后一个不与指定字符集合中的任何字符匹配的字符,并返回其位置。
除了以上几个方法外,还有查找满足指定条件的元素std::find_if,
std::find_if 是 C 标准库中的一个算法函数,用于在指定范围内查找第一个满足指定条件的元素,并返回其迭代器。需要注意的是,使用 std::find_if 函数时需要提供一个可调用对象(例如 lambda 表达式或函数对象),用于指定查找条件。
代码语言:javascript复制std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = std::find_if(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; });
if (it != vec.end()) {
std::cout << "Found even number: " << *it << std::endl;
}
此外,在业务开发有时候也会遇到需要C boost库支持的starts_with、ends_with。如果用C 标准库来实现,常规编写方法可如下:
代码语言:javascript复制bool starts_with(const std::string& str, const std::string& prefix) {
return str.compare(0, prefix.length(), prefix) == 0;
}
bool ends_with(const std::string& str, const std::string& suffix) {
if (str.length() < suffix.length()) {
return false;
} else {
return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
}
}
以上代码中,starts_with 函数和 ends_with 函数分别用于检查字符串的前缀和后缀。两个函数内部都使用了 std::string::compare 方法来比较字符串的子串和指定的前缀或后缀是否相等。如果相等,则说明字符串满足条件,返回 true;否则返回 false。
std::string与std::wstring转换
对字符串进行处理是一个很常见的业务场景,尤其是C 客户端开发,我们经常需要在窄字符串std::string与宽字符串std::wstring之间进行转换,有时候一不小心就会出现各种中文乱码。还有就是一提到窄字符串与宽字符串互转以及时不时出现的中文乱码,很多人就犯晕。
在 C 中,std::string和std::wstring之间的转换涉及到字符编码的转换。如果在转换过程中出现乱码,可能是由于字符编码不匹配导致的。要正确地进行std::string 和 std::wstring之间的转换,需要确保源字符串的字符编码和目标字符串的字符编码一致,避免C 中的字符串处理乱码,可以使用Unicode编码(如UTF-8、UTF-16或UTF-32)来存储和处理字符串。
我们想要处理或解析一些Unicode数据,例如从Windows REG文件读取,使用std::wstring变量更能方便的处理它们。例如:std::wstring ws=L"中国a"(6个八位字节内存:0x4E2D 0x56FD 0x0061),我们可以使用ws[0]获取字符“中”,使用ws[1]获取字符“国”,使用ws[2]获取字符“国”获取字符 'a' 等,这个时候如果使用std::string,ws[0]拿出来的就是乱码。
此外还受代码页编码的影响(比如VS可以通过文件->高级保存选项->编码 来更改当前代码页的编码)。
下面是一些示例代码,演示了如何进行正确的转换,针对Windows平台,官方提供了相应的系统Api(MultiByteToWideChar):
代码语言:javascript复制std::wstring Utf8ToUnicode(const std::string& str) {
int len = str.length();
if (0 == len)
return L"";
int nLength = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), len, 0, 0);
std::wstring buf(nLength 1, L'