C/C++常见问题(持续更新)

本文原载于https://imlogm.github.io,转载请注明出处~

摘要:记录在使用C/C++时遇到的常见问题:友元,计时,cout与printf,结构体内存对齐,虚函数,派生类,auto关键字,指针的sizeof,短路求值,qsort与sort,类的拷贝构造&&等号赋值&&深拷贝&&浅拷贝,输入函数cin

关键字:C/C++


1. 友元

关于友元函数,可以看这篇文章:【C++基础之十】友元函数和友元类-偶尔e网事

文章里说的友元的缺点是破坏封装性比较好理解,友元的优点是提高程序运行效率这点我没有太想明白。(为什么我感觉友元的优点是让程序员能少写一点代码?)

文章最后提到友元不具有继承性和传递性,记一下就好。实际编程时,我还没有遇到使用友元的情况。

2. 计时

在调试程序的时候,我们经常需要记录某一段代码运行的时间。我一般是使用clock()来计时的。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
#include <ctime>    // 必须添加的头文件,C++是ctime,C是time.h

int main() {
clock_t start = clock(); // clock_t 其实是long int
for (int i=0; i<1000000; i++); // 需要计时的程序段
clock_t end = clock();
double time_used = 1.0 * (end - start) / CLOCKS_PER_SEC;
printf("程序用时: %.5f秒", time_used);

return 0;
}

如果有过硬件开发经验的同学应该能明白,函数clock()获取的是滴答计数器的值。可以形象地理解:计算机的底层(或硬件或软件模拟)能不断地发出滴答声,并且滴答声的频率是稳定的,每秒发出CLOCKS_PER_SEC次滴答声。程序一开始运行,就有一个计数器启动,不断记录它听到的滴答声的次数。clock()获取到的就是这个计数器的值。clock() / CLOCKS_PER_SEC得到的就是从程序开始运行到当前经过几秒。

在我的电脑上,CLOCKS_PER_SEC=1000000,类型clock_tlong int的别名,在不同的硬件上,这些值不一定相同。

注意:这种计时方式有最大可计时时间。我们可以算一下,long int型最大为$2^{31}$,$2^{31} \div 1000000=2147$,2147秒大约是35分钟。也就是说程序运行后,每过大约35分钟,clock()取到的值会再次回到0。网上也有教程说最大计时时间为72分钟,可能是当成了unsigned long在算了。

3. cout与printf

cout比printf要方便一点,因为可以不用写格式控制字符串%d%s等。重载运算符后也能让cout输出非基本类型的变量。

但是printf比cout在IO效率上要更高一些,当然也有方法可以弥补这点,可以看这篇文章:cin与scanf cout与printf效率问题

4. 结构体内存对齐

这个涉及到比较底层的问题,用来解释sizeof(结构体)得到的结果为什么和直观想的不一样。可以看这篇文章:如何理解 struct 的内存对齐?-Courtier的回答

注意,形似下方代码的东西不是普通结构体,是位域,它的内存占用分析更复杂,这里就不展开了:

1
2
3
4
5
6
struct bs   // 位域
{
 int a:8; // 8表示占用8个bit
 int b:2;
 int c:6;
};

5. 虚函数

定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。

定义一个函数为纯虚函数,才代表函数没有被实现。纯虚函数定义如下:

1
virtual void fun()=0;

6. 关于派生类

  • 公有继承的公有成员还是公有的,可以被访问
  • 公有继承的私有成员不被继承,所以不能访问
  • 公有继承的保护成员可以被类的方法访问,不能被对象访问
  • 私有继承的公有成员会变成派生类的私有成员,也不能被访问

7. auto关键字

auto关键字在C++11以后有了不同于以前的含义。以前,auto关键字用来表示该变量是具有自动存储期的局部变量。如下面代码所示:

1
2
3
4
// C++11标准以前,auto关键字的用法
int a; // 平时,auto关键字可省略
auto int a; // auto表示有自动存储期的局部变量,与上句效果相同
static int a; // 静态变量没有自动存储器

C++11以后,auto表示该变量的类型由编译器推理。如下面代码所示:

1
2
3
// C++11标准以后
int a;
auto b = a; // b没有类型,但编译器会自动推理出b的类型为int

新标准下的auto主要在定义STL的迭代器时用到,因为这些迭代器类型名字都太长了(比如std::vector<std::string>::iterator),使用auto关键字可以少打很多字。

8. 指针的sizeof

这其实是一个值得注意的地方,因为老教材通常默认是32位机器。32位程序,sizeof(指针)=4;64位程序,sizeof(指针)=8

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
int a = 0;
int* p = &a;
std::cout << sizeof(p) << std::endl;
return 0;
}

9. 短路求值

在用到&&||时会有短路求值的情况出现。比如下面的代码:

1
2
3
4
5
6
7
// && 的短路求值示例
// 先判断isClothes(a),若为false,则不再判断isRed(a)
if (isClothes(a) && isRed(a)) {
printf("a为红色衣服");
} else {
printf("a不为红色衣服");
}

在代码执行的效率上说,短路求值能避免一些无意义的计算,但是短路求值也会有坑。

假如函数isRed(a)会对全局变量进行操作,或者有指针操作,就会出现问题,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int red_count = 0;  // 全局变量,记录红色物品的数量

int main() {
// && 的短路求值示例
// 先判断isClothes(a),若为false,则不再判断isRed(a)
if (isClothes(a) && isRed(a)) {
printf("a为红色衣服");
} else {
printf("a不为红色衣服");
}

printf("\nred_count的值:%d", red_count);

return 0;
}

// isRed函数的实现
bool isRed(object a) {
if (a是红色) {
++red_count;
return true;
} else {
return false;
}
}

应该不难看出上述代码的问题出在red_count,由于短路求值,isRed(a)仅当isClothes(a)true时执行,也就是说red_count只记录了红色衣服的数量,而我们本来是希望记录所有红色物体的数量的。

有可能会有同学说,上面的代码逻辑不规范,我自己写肯定不会这样统计红色物体数量。是的,一般都不会这么写代码,我这边这样写是为了说明短路求值会带来的问题。实际情况中,往往是涉及指针操作时会出现。

一些“简洁精妙”的代码,以“行数少”著称。它们&&||两边的函数往往是精心设计,不能调换次序的。我们平时写代码的时候,没必要刻意追求“行数少”。如果“行数少”,但复杂度还和原来一样,那么这样的“行数少”有什么意义呢?反而造成阅读代码的困难。我们真正要追求的是复杂度的降低,和代码的易阅读性。

10. qsort与sort

qsort与sort都是C/C++中的排序函数,其中qsort属于库stdlib.h,sort属于库algorithm,它们都只能对连续内存的数据进行排序(比如数组等),对不连续内存的数据不能排序(比如链表)。

有关这两个函数的用法,请看:qsort函数、sort函数-JokerSmithWang的博客

11. 类的拷贝构造&&等号赋值&&深拷贝&&浅拷贝

这块内容需要一定的时间消化,参见:C++ 拷贝构造函数和赋值运算符-Brook的博客

12. 输入函数cin

平时读取字符串一直用的是std::cin >> str读入输入的,但是这种方式如果字符串里有空格,那么会把空格视为分隔符,把原字符串分成多个字符串。如果要实现读取带空格的字符串,可以使用std::cin.getline(),具体使用方式可以参见:C++输入cin,cin.get(),cin.getline()详细比较及例子-Mac Jiang的博客

0%