文章

从缺陷中学习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 判断,以避免错误调用。

本文由作者按照 CC BY 4.0 进行授权