从缺陷中学习C++
0.前言
之前我的博客中还写道,有一本C++踩坑经验的书就好了,站在前人的肩膀上更有效率的coding,还真被我遇到了
本来在读这本书的时候,没想写笔记,但是有时遇到挺有意思的问题时,就忍不住想记录下
1.基础问题
由于基础知识不清楚导致的问题
1.1 宏展开问题
1
2
3
4
#define SQR(x) ((x) * (x))
i = 3;
SQR(++i);
我们本意是要得到4*4=16
但其实展开后为4*5=20
总结:
C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的
数据成员
在 C++ 程序中,应该用内联函数取代所有宏代码,只有”断言 assert”例外,assert
是仅在 Debug 版本起作用的宏,它用于检查”不应该”发生的情况。
1.2 整除的精度问题
1
2
float result
result = 1/6; // 0
正确的
1
result = 1.0/6.0;
1.3 临时变量溢出
1
2
3
4
5
6
long multiply(int m, int n)
{
long score;
score = m*n;
return score;
}
当m,n都取1亿(在int范围内),这时候结果溢出,因为m*n的结果会先保存在一个int的临时变量中,而这个m*n的范围在long的范围内
正确的
1
2
3
4
5
6
long multiply(int m, int n)
{
long score;
score = static_cast<long>(m)*static_cast<long>(n);
return score;
}
1.4 浮点数比较问题
浮点数其表示精度的位数有限,于是不能准确的表示一个小数(IEEE 754 规定的单精度
float 数据类型的表示精度为 7 位有效数字,双精度 double 为 16 位有效数字),所以,在代
码中对浮点数据类型使用== 、<= 、>=、 !=等运算符都是不正确的
于是浮点运算考虑误差时一般使用的是相对误差,并不是绝对误差。不同数量级浮点数
据类型之间的运算所要考虑的误差的范围是不同的
1
2
3
4
5
double a = 0.1, b = 0.1;
double epsilon = 1e-9; // 一个非常小的值
if (fabs(a - b) < epsilon) {
// a 和 b 被认为是相等的
}
fabs是取绝对值
2.库函数问题
2.1 c_str问题
错误代码:
1
2
3
char* c;
string s="1234";
c = s.c_str();
c最后指向的是辣鸡,因为s对象后面会被析构
正确的
1
2
3
char c[20];
string s="1234";
strcpy(c,s.c_str());
2.2 strcpy()问题
错误代码:
1
2
3
std::string buffer = "ab\0c";
char* str_array = new array[buffer.size()];
strcpy(str_array, buffer.c_str());
会造成复制后str_array和buffer内容不一样
因为字符串在内存是二进制流,对二进制流来说\0是个正常的01序列,但\0对string对象来说是个特殊字符,用来标识字符串的终结符
因此对字符串函数strcpy等函数来说,其操作的范围是字符串的开始碰到一个\0结束
正确做法应该用memcpy,它是对内存的拷贝,因为不管什么都是01序列,所以\0也没有什么特殊
正确代码:
1
memcpy(str_array, str.c_str(), length);
注意:memcpy 中的 length 是 buffer 的实际长度,不等价于 str.size(),因为 string 长度也是
以\0 为结束所作的计算。
使用 string 来作为二进制 buffer 的时候,一定要注意内容中包含\0。
2.3 题外话一 c_str()
头文件:<string.h> 和 <cstring>
参数:无
返回值:指向C语言字符串表示形式的指针
为什么有c_str的存在呢,因为让字符串从C++形式转为C形式的字符串,创建具有相同字符序列的字符数组,这对于需要用C形式字符串做输入参数的旧C函数和API很有用
1
2
std::string myString = "Hello";
const char* cString = myString.c_str();
这样的cString就是指向基础字符数组的指针,现在cString指向序列为“H”、“e”、“l”、“l”、“o”、“\0”的字符数组。’\0’ 是表示字符串末尾的 null 终止符。
为什么要用它呢
- 和C的API互动
很多C语言库都希望字符串作为char* 而不是std::string传递,c_str()就可以轻松将C++字符串传递给C函数
- 兼容性
一些较旧的 C++ 库是在 std::string 存在之前编写的,并且仍然需要 char* 字符串。c_str() 允许将这些库与现代C++字符串类型一起使用
- 性能
一些对性能非常关键的代码可能会使用 char* 而不是 std::string 来加快速度。c_str()可以在需要时灵活地执行此操作。
- 调试
在调试C++代码时c_str() 可以轻松查看字符串的基础字符数据。这有助于确保字符串包含所需的数据。
详细看这个C++ String c_str() 函数 - Coding Ninjas
2.3题外话二 strcpy()
语法:
1
char* strcpy(char* dest, const char* src);
strcpy(first, second);
头文件是 <string.h> 和 <cstring>
参数:
是把第二个字符串复制到第一个字符串上
返回值:
返回指向第一个字符串的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char str1[] = "Sarah came first in her exams";
char str2[] = "John came first in his exams";
strcpy(str1, str2);
cout<<str1<<"\n";
char str3[100];
strcpy(str3,"What a beautiful day!");
cout<<str3<<"\n";
return 0;
}
输出
1
2
John came first in his exams
What a beautiful day!
注意
1.它的入参有两个,都是C样式的字符串,这个函数不接受C++风格字符串作为输入
2.它是创建src的指针引用的字符串的副本,因此它不会影响源字符串
3.第一个字符串要比第二个字符串大
2.4字符串比较
1
2
3
char *p = "test";
char *q = "test";
if(p == q)
这比的是两个指针的内容,也就是两个字符串所存放的地址,因为有些编译器为了省事,所以test这个相同内容就被存到同一份地址上,虽然if判断是错的,但是看不出来,结果还是对的。
所以如果比较两个字符串,定义成string就不用担心了
还有另一种情况
1
2
3
char a[5] = "test";
char b[5] = "test";
if(a == b)
这比的是字符型数组,其实是比数组的地址,所以肯定是false
正确是用strcmp等字符串函数,strcmp是C语言的函数
入参是
1
int strcmp (const char* str1, const char* str2);
返回值是
0 相等
>0 str1第一个不匹配的字符ASCII大于str2
<0 str1第一个不匹配的字符ASCII小于str2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "abcd", str2[] = "abCd", str3[] = "abcd";
int result;
// comparing strings str1 and str2
result = strcmp(str1, str2);
printf("strcmp(str1, str2) = %d\n", result);
// comparing strings str1 and str3
result = strcmp(str1, str3);
printf("strcmp(str1, str3) = %d\n", result);
return 0;
}
输出是
1
2
strcmp(str1, str2) = 1
strcmp(str1, str3) = 0
2.5 strncpy和strcpy都尽量不要用
前者复制时,不会复制\0,后者复制则要注意src(带上\0)不要超过dest,src是第二个入参,dest是第一个入参,是要复制的目标地
2.6 vector的erase问题
错误的代码:
1
2
3
4
5
6
7
8
9
10
11
12
std::vector<int> int_vec;
int_vec.push_back(1);
int_vec.push_back(2);
int_vec.push_back(2);
int_vec.push_back(3);
for (std::vector::iterator it = int_vec.begin(); it != int_vec.end(); ++it)
{
if (*it == 2)
{
int_vec.erase(it);
}
}
按理说,erase后it第一个2已经被删除掉了,所以删除后的it是指向第二个2的,但是循环++it后,此时it指向的是3,相当于跳过了一个2
所以正确代码是
1
2
3
4
5
6
7
8
9
for (std::vector::iterator it = int_vec.begin(); it != int_vec.end();)
{
if (*it == 2)
{
int_vec.erase(it);
continue;
}
it++;
}
正常到这里就结束了,但是我在VS测试时发现正确的代码还是会报错,那是为什么呢?
其实来说erase返回的是一个新的迭代器,也就是把当前元素后面的元素都移动后的迭代器,但是erase并不会自动更新it,所以你还用这个it的话,其实这个迭代器是不对的,是老的迭代器,但神奇就在于linux状态下上面的代码是对的…就离谱
真正完全正确的方法是,在stackoverflow上也看到这个写法印证了我的想法,否则用老的it在VS上会产生未定义的行为
1
2
3
4
5
6
7
8
9
for (std::vector::iterator it = int_vec.begin(); it != int_vec.end();)
{
if (*it == 2)
{
it = int_vec.erase(it);
continue;
}
it++;
}
为什么会造成这个原因,朋友调试了下发现虽然VS下虽然老迭代器还是指向正确的下一个元素,但是老迭代器里面某些作为迭代器的数据元素被破坏了,所以用它的话VS会出错
3.逻辑问题
3.1vector为空却产生不存在的指针
错误代码
1
2
3
4
5
void func(const int* pInt, size_t size);
…
std::vector<int> vecInt;
…// 对 vecInt 进行操作
func( &vecInt[0], vecInt.size() );
当 vector 中没有元素,&vecInt 试图产生一个不存在的指针,而当在函数中操作这样一个指针,只要进行取值操作就会造成程序的奔溃。
正确代码
1
2
3
4
5
6
7
8
9
if (!vecInt.empty()) {
func(&vecInt[0], vecInt.size());
}
或者
int func(const int* pInt, size_t size)
{
if (pInt==NULL) return -1;
…
}
对于指针的操控前需要判断指针不为 NULL,特别是数组或 vector 容器复杂的初始化逻辑中,需要考虑到是否有未被赋值的逻辑;对于函数中的 C 指针参数,使用前需要 NULL 判断,以避免错误调用。