该系列主要用于收集我每天在各种地方不论是刷题还是面经,看到的一些小的,易忘的或者还没见过的知识点。

今日所有全是牛客C++选择题

[TOC]

1. switch和case的小细节

  • case穿透

在C++的switch语句中,当表达式的值与某个case标签匹配时,程序会从该点开始执行,并持续执行后续所有代码(包括其他case标签下的语句),直到遇到break或整个switch结束,这种特性称为“case穿透”。如果表达式的值与所有case都不匹配,程序则直接跳过整个switch块,不会执行任何case内的代码;为了避免这种静默跳过,应使用default分支作为默认处理逻辑。

2. 做了多少遍还是错的指针运算

1
2
3
4
5
char a[2][2] = {{'a','b'},{'c','d'}};
char (*p)[2] = a;
cout<<*(*(p+1));
p++;
cout<<*(*p+1)<<endl;

p 是一个指向包含2个char的数组的指针,初始指向a[0](即第一行数组{'a','b'}

  • p 的类型是 char (*)[2]p+1 的指针运算:p 增加的是它指向类型的大小,即 sizeof(char[2]) = 2 字节。
  • 所以 p+1 指向 a[1](第二行数组{'c','d'})。
  • *(p+1) 就是取这个地址的内容,得到 a[1](等价于 &a[1][0],类型 char*)。
  • *(*(p+1)) 等价于 a[1][0],即 **'c'**。

第二个输出 cout<<*(*p+1);

  • p 当前指向 a[1]{'c','d'})。
  • *p 得到 a[1](类型 char*,指向 a[1][0]'c' 的地址)。
  • *p + 1 指针运算:char* 加 1,偏移 sizeof(char) = 1 字节,指向 a[1][1](即 'd' 的地址)。
  • *(*p+1) 解引用得到 **'d'**。

3. 仅返回值类型相同不会构成函数重载!

4. 陷阱:a<b<c?

1
2
3
4
5
6
7
8
9
10
11
int main() {
int a = 1, b = 3, c = 2;
while (a < b < c)
{
++a;
--b;
--c;
}
cout << a << " " << b << " " << c << "\n";
return 0;
}

这道题的核心陷阱在于C++并不支持数学中的链式比较,while (a < b < c)的实际执行逻辑是先计算(a < b)得到一个布尔值(true转为1,false转为0),再将这个整数结果与c进行比较。因此循环的持续条件并非直观上的“a小于b且b小于c”,而是((a < b)的结果) < c。初始时(1<3)为true(1),1<2成立进入循环,经过三次迭代后变量变为a=4,b=0,c=-1,此时(4<0)为false(0),而0<-1为false,循环终止,最终输出4 0 -1

5. delete重复释放的风险在哪?

我以为重复释放有风险是指对空指针重复释放,原来不是啊。

重复释放的风险主要指对非空指针(已释放但未置空的指针)再次调用 delete,这会导致严重的未定义行为(程序崩溃、数据损坏等)。而对空指针(nullptr)重复释放是安全的,因为 delete nullptr; 被标准定义为无操作(no-op)。

一个关键但常被误解的点是:delete 仅释放指针指向的内存,而不会改变指针变量本身的值。执行后指针仍保留原地址成为“悬垂指针”,若再次解引用或重复删除将引发未定义行为,必须手动置空来避免。

总结来说,重复释放的真正风险在于对非空悬垂指针进行二次delete,这会导致严重的未定义行为(如程序崩溃或堆损坏),因为指针仍指向已被系统回收的内存区域;而对空指针(nullptr)重复释放是完全安全的,因为C++标准明确将delete nullptr;定义为无操作(no-op)。这一区别的关键在于,delete操作本身不会自动将指针置空,若我们误以为它会自动置空,就可能省略手动置空步骤,从而在后续代码中意外使用悬垂指针进行释放或访问,引发隐蔽的错误。

6. 前置后置递增的运算符重载++

C++ 中运算符重载的关键区别在于:前置递增(如 ++obj)应返回被递增后的对象本身,因此通常声明为 myclass& operator++();而后置递增(如 obj++)需要返回对象递增前的原始值作为副本,并且为了与前置版本进行语法区分,必须添加一个无用的 int 类型参数(调用时编译器自动传递 0),因此声明为 myclass operator++(int)。这个 int 参数纯粹是一个占位符,它并不参与实际运算,只用于编译器识别这是后置版本。在实际实现中,后置运算符通常需要先创建原对象的临时副本,然后对原对象执行递增操作,最后返回那个临时副本(以值形式),从而模拟“先使用原值,再递增”的语义。

7. 析构函数不能有形参

析构函数之所以不能有任何形参,其根本原因在于C++对象生命周期的自动管理机制:析构函数的调用完全由编译器在对象离开作用域、被delete或作为临时对象结束时自动插入,这些调用点没有任何上下文信息能够提供参数值,因此语言强制规定析构函数必须无参,以确保所有对象都能以统一且确定性的方式被安全销毁。如果允许参数,编译器将无法生成正确的销毁代码,导致对象在析构时行为不确定,破坏资源管理的可靠性,这也体现了C++的设计哲学——构造函数可以有多种初始化方式(可重载),但销毁过程必须是唯一且不可定制的,从而保证资源在任何情况下都能被正确释放。

8. 恨恨恨!C/C++中字符数组声明和初始化!

每次碰到这种题我就烦啊!感觉花样太多,每次做题都错!

总结!

声明方式 数组大小 内容 结尾 \0 可否修改 常见错误
char str1[] = "hello"; 自动计算(6) hello 自动添加 可修改
char str2[6] = "hello"; 6(显式指定) hello 自动添加 可修改 [5]会出错(空间不够)
char str3[5] = "hello"; 5 hello 没有空间放 可修改 ⚠️不是合法字符串(无\0
char str4[10] = "hello"; 10 hello 自动添加,后补\0 可修改 浪费空间但安全
char str5[] = {'h','e','l','l','o'}; 5(自动计算) hello 没有 可修改 ⚠️不是合法C字符串
char str6[] = {'h','e','l','l','o','\0'}; 6(自动计算) hello 手动添加 可修改
char *str7 = "hello"; 无(指针) hello 只读(字面量在常量区) 试图修改会崩溃
const char *str8 = "hello"; 无(指针) hello 明确只读(推荐写法)

口诀:

双引号自动补零,单引号手动补零; 方括号是自家院,星号指向别人田; 常量区里不能改,栈上数组随便来。

  • "..." 初始化会自动加 \0
  • {'a','b'} 初始化要手动加 \0
  • char name[] 是自己的数组(在栈上)
  • char *name 是指向别人的指针(可能指向只读区)
  • 字面量在常量区不可修改
  • 栈上的数组可以随意修改内容

9. sizeof(void)

在C/C++中,void类型代表“无类型”,它用于表示函数不返回值或作为通用指针类型(void*),但**void本身不是一个完整类型,不占用任何存储空间**,因此sizeof(void)在标准C/C++中是无效的语法,会导致编译错误。无论是32位还是64位编译器,都不允许对void类型使用sizeof运算符,这与指向void的指针(void*)完全不同——后者在32位系统下固定为4字节,其大小取决于系统位数而非指向的类型。这一设计体现了类型系统的严格性:只有具有明确大小的完整类型才能计算sizeof,而void作为类型占位符不具备这一属性。