《重学C++》4. C++基础容器

1.数组下标非对称的原则

计算“满足x>=16且x<=37的x有多少个”时,容易计算成37-16=21个。
由于差一错误(off-by-one error),遍历序列容器、解决问题时最好使用数学上的左闭右开[a,b)的区间。
问题可以转换为:“满足x>=16且x<38的x有多少个”,此时计算得38-16=22个。
应该尽量全部转换为左闭右开[a,b)的区间,保证问题和解题的统一性,避免出错。

1
2
3
4
5
6
7
8
9
//推荐方式
for (int index = 0; index < 10; ++index)
cout << a[index] << " ";
cout << endl;

//不推荐方式:没有统一左闭右开[a,b)的区间、后置++降低了性能
for (int index = 0; index <= 9; index++)
cout << a[index] << " ";


好处是:

  • 上界永远>=下界;
  • 便于计算 容量=上界-下界;
  • 上界永远取不到值,能避免数组越界。

另外,我注意到C++中其他很多地方也是贯穿了这个原则,例如vector的begin()、end()函数,begin()是下界、可以取到,end()是上界、取不到。因此,和C++语言保持一致,统一用这种方式也更加便利。


2. 二维数组访问

因为cpu会将经常访问的内容放入cache,根据这个原理,在短时间内应该尽量访问相邻的空间。这就是为什么一般不会跨行访问(即外层循环行,内层循环列)。

此外,一般也将长的循环放在内层、短的循环放在内层。这样可以缩短cpu跨切循环层的次数。

3. vector

vector是面向对象方式的动态数组。
通过capacity和size函数,可以分别获取数组的容量(下次申请空间前,最多可以存储几个元素)和存储的元素个数(当前数组中有几个元素)。

string和vector类似,也有这两个函数,并且功能相同。另外还有一个和size函数功能相同的length函数。

4. 字符集

unicode是字符集,解决了自然字符数字化的问题;
UTF-8是编码方式,解决了数字字符在计算机中传播、存储的问题。

出现乱码的原因在于编码和解码方式的不统一。HTML、XML等文件能够被广泛传播的原因在于,他们文件一开头就指明了编码方式。而传统的文件并没有考虑到这点(但开头可能有big endian、BOM标记等)。

5. 字符串的数组表示和字符指针表示

对于下面这段代码,尽管他们都表示字符串“helloworld”,但底层原理并不相同。

1
2
3
4
5
//第一行是字符串的数组表示、第二第三行是字符串的指针表示。
char strHelloWorld[11] = { "helloworld" }; // 字符串的数组表示
char* pStrHelloWrold = "helloworld"; //pStrHelloWrold第一次赋值,指针变量的值允许改变,但此时指针变量
pStrHelloWrold = strHelloWorld; //pStrHelloWrold第二次赋值

执行下面代码对各元素进行+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//strHelloWorld = pStrHelloWrold;  错误,strHelloWorld作为数组不可改变
// 通过数组变量遍历修改数组中的元素值
for (int index = 0; index < strlen(strHelloWorld); ++index)
{
strHelloWorld[index] += 1;
std::cout << strHelloWorld[index] << std::endl;
}

// 通过指针变量遍历修改数组中的元素值,pStrHelloWrold赋值前后运行结果不同
for (int index = 0; index < strlen(strHelloWorld); ++index)
{
pStrHelloWrold[index] += 1;
std::cout << pStrHelloWrold[index] << std::endl;
}

字符数组StrHelloWorld不可变,strHelloWorld[index]的值可变;
字符指针pStrHelloWrold可变,pStrHelloWrold[index]的值可变不可变取决于所指区间的存储区域是否可变;

数组strHelloWorld将里面每个元素值+1。而尽管pStrHelloWrold一直是字符指针,但是对于它的两个不同的值,+1操作的结果是不同的。

当pStrHelloWrold指向”helloworld”,此时该指针指向字符串常量区域,对它的字符进行+1操作会引发写入访问权限错误;
当pStrHelloWrold指向strHelloWorld,此时该指针指向字符串变量区域,进行+1操作可以和数组strHelloWorld一样,将各字母再次增大1。


6. C字符串函数与缓冲区溢出

传统的strcpy、strcat等函数并不会在操作前对字符串长度进行越界检查,这就导致可能会出现缓冲区溢出,产生安全漏洞。

为了避免这种问题,应该尽量使用strnlen_s,strcpy_s,strncpy_s,strcat_s等API函数,更加安全。

使用strcat函数将“Welcome to C++”追加到字符串str3后,由于长度超限覆盖了str1

7. string

C++中字符串string具有了更多功能,如动态分配大小、拷贝=、比较、拼接+等等。
string和vector类似,也有size和capacity两个函数,并且功能相同。另外还有一个和size函数功能相同的length函数。
需要注意的是,即使是未初始化的空字符串,其capacity也是大于0的。

在对性能要求高的场所,还是需要使用C字符串,string并不是万能的。(c_str对象函数可以返回C字符串的指针)。
并且,Redis(https://redis.io/)字符串的设计对C字符串空间和时间进行了折中。

另外,字符串的拼接不可以拼接两个字符串常量

1
2
3
4
string str = "abc";
string str1 = str + “def";
string str2 = "abc" + str;
string str3 = "abc" + "def"; //错误!