c++常见问题¶
1. 段错误(Segmentation Fault)常见原因¶
段错误(Segmentation Fault, 简称Segfault)是程序试图访问未被允许访问的内存区域时发生的错误。以下是可能导致段错误的常见原因:
空指针解引用
C | |
---|---|
1 2 |
|
访问已释放的内存
C | |
---|---|
1 2 3 |
|
数组越界访问
C | |
---|---|
1 2 |
|
栈溢出
C | |
---|---|
1 2 3 |
|
修改字符串常量
C | |
---|---|
1 2 |
|
错误的指针运算
C | |
---|---|
1 2 3 |
|
错误的类型转换
C | |
---|---|
1 2 3 |
|
多线程竞争条件 多个线程同时访问同一内存区域,可能导致不可预测的行为。
调试段错误的工具和方法
-
gdb:GNU调试器,可以定位段错误发生的位置
Text Only 1 2
gcc -g program.c -o program gdb ./program
-
valgrind:内存调试工具
Text Only 1
valgrind ./program
-
核心转储文件:分析程序崩溃时的内存状态
Text Only 1 2 3
ulimit -c unlimited ./program # 崩溃后生成core文件 gdb program core
-
打印调试:在关键位置添加打印语句
预防段错误的最佳实践包括:始终初始化指针、检查内存分配是否成功、避免越界访问、谨慎使用指针运算等。
2. 智能指针如何克服裸指针的问题¶
智能指针是C++中管理动态内存的RAII(资源获取即初始化)对象,它们克服了裸指针的许多常见问题:
2.1 内存泄漏问题¶
裸指针问题:需要手动delete
,容易忘记释放内存
C++ | |
---|---|
1 2 3 4 5 |
|
智能指针解决:自动释放内存
C++ | |
---|---|
1 2 3 4 |
|
2.2 悬垂指针问题¶
裸指针问题:访问已释放的内存
C++ | |
---|---|
1 2 3 |
|
智能指针解决:
- unique_ptr
:独占所有权,不可能出现悬垂指针
- shared_ptr
:引用计数,最后一个引用消失时才释放
- weak_ptr
:不增加引用计数,可检测对象是否已被释放
2.3 所有权不明确问题¶
裸指针问题:无法明确谁负责释放内存
C++ | |
---|---|
1 2 3 4 5 |
|
智能指针解决:明确所有权语义
C++ | |
---|---|
1 2 3 4 5 6 |
|
2.4 异常安全问题¶
裸指针问题:异常可能导致内存泄漏
C++ | |
---|---|
1 2 3 4 5 |
|
智能指针解决:异常安全
C++ | |
---|---|
1 2 3 4 5 |
|
2.5 多线程安全问题¶
裸指针问题:多线程环境下难以安全共享数据
C++ | |
---|---|
1 2 3 4 5 6 7 |
|
智能指针解决:
C++ | |
---|---|
1 2 3 4 |
|
主要智能指针类型及其用途¶
unique_ptr
:- 独占所有权
- 轻量级,零开销
-
不可复制,只能移动
-
shared_ptr
: - 共享所有权
- 引用计数
-
可复制,较重的开销
-
weak_ptr
: - 不增加引用计数
- 解决shared_ptr循环引用问题
- 必须转换为shared_ptr才能访问对象
智能指针通过自动化内存管理和明确所有权语义,大大减少了C++中与裸指针相关的常见错误,是现代C++内存管理的首选方式。
3. NULL和nullptr¶
比较项 | nullptr |
NULL |
---|---|---|
类型 | std::nullptr_t |
宏(通常是 0 或 ((void*)0) ) |
是否类型安全 | ✅ 类型安全 | ❌ 可能与整型混淆 |
C++11 引入 | ✅ | ❌(C/C++都有) |
推荐程度 | ✅ 推荐(现代 C++) | ❌ 不推荐(已过时) |
C++ | |
---|---|
1 2 3 4 5 |
|
⚠️ NULL 的问题
在 C++ 中 NULL
通常是 #define NULL 0
,即它是 int
类型,因此会出现函数重载歧义。
为什么 nullptr
更好
nullptr
是一个真正的“空指针”类型
C++ | |
---|---|
1 2 |
|
nullptr
不会和整型冲突
C++ | |
---|---|
1 2 3 4 5 6 |
|
场景 | 推荐做法 |
---|---|
C++11 及以后 | 使用 nullptr 替代 NULL |
与 C 库接口 | 可以继续使用 NULL (兼容) |
判断指针是否为空 | if (ptr == nullptr) ,不要写 if (ptr == NULL) |
如你用的是 C++11 及以上版本(建议默认使用),请避免使用 NULL
,统一用 nullptr
。这样可以避免二义性和类型错误
4. 指针对比¶
这三个看起来很相似的 C/C++ 声明:
C++ | |
---|---|
1 2 3 |
|
语法结构不一样,含义完全不同。下面我逐个为你清晰解释。
4.1 int *p[10];
¶
p
是一个数组,这个数组有 10 个元素,每个元素是int*
类型(即指向 int 的指针)。
Text Only | |
---|---|
1 2 3 4 |
|
C++ | |
---|---|
1 2 3 4 |
|
4.2 int (*p)[10];
¶
p
是一个指针,这个指针指向一个包含 10 个int
元素的数组。
Text Only | |
---|---|
1 |
|
举例使用:¶
C++ | |
---|---|
1 2 3 |
|
这种写法通常用于二维数组、传递数组引用等场景。
4.3 int *p(int);
¶
p
是一个函数,它接受一个int
类型的参数,返回一个int*
类型的值。
C++ | |
---|---|
1 2 3 4 |
|
也可以是函数声明:
C++ | |
---|---|
1 |
|
4.4 int (*p)(int)
¶
p
是一个函数指针,它指向一个函数,这个函数接收一个int
参数,返回一个int
类型的值。
- 先看
p
:它被(*p)
包裹 →p
是指针 - 然后看外面的
(int)
→ 这个指针指向一个以int
为参数的函数 - 最左边
int
是这个函数的返回类型
所以整体意思:
Text Only | |
---|---|
1 |
|
C++ | |
---|---|
1 2 3 |
|
C++ | |
---|---|
1 2 |
|
C++ | |
---|---|
1 2 |
|
函数指针在实际用途
- 回调函数(如 sort 比较器)
- 接口函数映射
- 动态选择算法(策略模式)
- 事件处理机制
声明 | 含义 |
---|---|
int *p[10]; |
数组:包含 10 个指向 int 的指针 |
int (*p)[10]; |
指针:指向一个 int[10] 的数组 |
int *p(int); |
函数:接受一个 int 参数,返回一个 int* |
int (*p)(int); |
函数指针:p 是指针,指向一个接受 int 参数、返回 int 的函数 |
🔧记忆小技巧¶
规则说明 | 示例 |
---|---|
[] 优先级比 * 高 → int *p[10] 是数组,不是指针 |
✅ |
() 包住 * 可以改变结合顺序 → (*p)[10] 是指针,指向数组 |
✅ |
p(int) 说明是个函数 |
int *p(int) 是函数,返回 int* |
(*p)(int) 表示函数指针 |
p 是指向函数的指针,返回 int |
5. c++/C的区别¶
- c++中new/delete是对内存分配的运算符, 取代了C中的malloc/free;
- 标准C++中的字符串取代了C函数库头文件中字符数组处理函数,C中没有字符串类型;
- C++中用来做控制输入输出的iostream类库取代了标准C中的stdio函数库;
- c++中的try/catch/throw异常处理机制取代了C中的setjmp()和ongjmp()函数;
- 在c++中,允许有相同的函数名, 不过它们的参数类型不能完全相同,这样这些函数可以区别开来,而这些C语言中事不允许的, 也就是C++可以重载, C语言不允许;
- c++语言中,允许变量定义语句在程序中的任何地方,只要是在使用它之前就可以;c语言中,必须要在函数开头部分,而且C++不允许重复定义变量, C语言做不到这一点;
- 在c++中,除了值和指针之外,新增了引用。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的;
- c++相对于c增加了一些关键字, 如:bool, using, dynamic_cast, namespace等;
6. extern"C"
用法¶
extern "C"
是 C++ 中的一个语言连接规范声明,主要用于解决 C++ 与 C 语言混编时的链接兼容性问题。
它告诉编译器:按照 C 语言的方式来处理被包裹的函数或变量名,不进行 C++ 名字修饰(name mangling)。
防止 C++ 编译器对函数名进行重整(name mangling),使得 C++ 能调用 C 编译的函数,或让 C 能调用 C++ 的函数。
例如下面这个 C++ 函数:
C++ | |
---|---|
1 |
|
在 C++ 编译后,函数名可能变成这样:
C++ | |
---|---|
1 |
|
而 C 语言中不做这种处理,函数名就简单是 add
。
6.1 声明一个 C 风格的外部函数¶
C++ | |
---|---|
1 |
|
告诉 C++ 编译器:add
是一个 C 风格的函数,不要做 name mangling。
6.2 在头文件中包裹 C 接口(推荐写法)¶
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
✅ 这样可以让
.h
头文件同时被 C 和 C++ 包含,安全通用。
假设有一个 C 文件:
C | |
---|---|
1 2 3 4 5 6 |
|
对应的头文件:
C | |
---|---|
1 2 3 4 5 6 7 |
|
然后在 C++ 中调用:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
C++ | |
---|---|
1 |
|
7. 野指针和悬空指针¶
野指针:没有被初始化过的指针
C++ | |
---|---|
1 2 3 4 5 |
|
悬空指针: 指针最初指向的内存已经被释放了的一种指针
C++ | |
---|---|
1 2 3 4 5 6 7 8 |
|
p=p2=nullptr
,此时再使用,编译器会直接报错。避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,本质就是避免悬空指针的产生
8. 浅拷贝和深拷贝¶
浅拷贝
浅拷贝只是拷贝的一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向的同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针资源就会出现错误。
深拷贝
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原来的对象被析构,释放的内存也不会影响到深拷贝的值。在自己实现拷贝赋值时,如果有指针变量需要自己实现深拷贝
C++ | |
---|---|
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源后,拷贝对象析构时再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,各自成员地址不同。
9.c++的异常处理方法¶
常有的异常有: - 数组下标越界 - 除法计算时除数为0 - 动态分配空间不足
9.1 try/throw/catch关键字¶
关键字 | 作用 |
---|---|
try |
定义一个可能抛出异常的代码块。必须和 catch 搭配使用。 |
throw |
抛出一个异常,可以是内建类型或自定义类型。 |
catch |
捕获异常,并定义如何处理该异常。 |
cpp中的异常处理机制主要使用try, throw和catch三个关键字,其在程序中的用法如下:
C++ | |
---|---|
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 26 27 28 29 |
|
10 静态变量什么时候初始化¶
- 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存
- 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但是C和C++中静态局部变量的初始化节点又有点不一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化。所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全局回收;
- 而在C++中,在执行相关代码时才会初始化,主要是由于C++引入对象后,要进行初始化执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单的分配内存,所以C++标准定为全局或静态对象是有首次用到时才会进行构造,所以在C++中是可以使用变量对静态局部变量进行初始化;
11 malloc/realloc/calloc的区别¶
malloc
C++ | |
---|---|
1 2 |
|
calloc
C++ | |
---|---|
1 2 |
|
省去了人为空间计算,malloc申请的空间的值是随机的, calloc申请的空间的值是初始化为0的;
realloc
C++ | |
---|---|
1 |
|