易错点
字符串
| 类型 | 推荐方法 | 说明 |
|---|---|---|
int a[5](裸数组) |
std::size(a) |
C++17,最清晰 |
int a[5] |
sizeof(a)/sizeof(a[0]) |
经典,但仅限未退化时 |
int a[5] 传进函数 |
❌ 拿不到 | 必须额外传 size |
std::array<int,5> a |
a.size() |
任何时候都行 |
std::vector<int> v |
v.size() |
任何时候都行 |
std::string s |
s.size() |
任何时候都行 |
char c[](C 字符串) |
strlen(c) |
不含 '\0' |
string与char数组的区别
简单一句话回答:在“访问方式”上很像,在“本质和规则”上完全不同。
作为初学者,你可以把string理解为带有各种高级功能的超级 char 数组,但千万别把它当成真正的 char 数组去用(这就是你上一题报错的原因)。
我帮你把它们的相似点和致命区别彻底理清楚:
✅ 相似点:为什么用起来像数组?
- 支持下标访问:
std::string重载了[]运算符。 - 内存是连续的:在 C++11 及以后的标准里,
string的底层确实是一块连续的字符内存,所以你可以用&str[0]拿到首地址。 - 可以无缝转换:你可以通过
str.c_str()或str.data()拿到 C 风格的const char*指针,直接传给那些只认 char 数组的老函数。
❌ 本质区别:为什么不能直接等同?
1. 身份不同:一个是“类”,一个是“原始内存”
char arr[10]是一块光秃秃的内存,它本身没有大小记录,没有自带方法。std::string是一个完整的类(对象)。它内部不仅存了字符,还存了“字符串长度”、“内存容量”等信息,并且自带了几十个好用的函数(比如.size(),.substr(),.find())。
2. 赋值规则不同(你上题报错的核心)
char arr[10]是极其死板的。你不能在定义之后用=给它赋值,更不能写成char arr[str.size()] = str;。std::string是非常智能的,它支持直接赋值、拼接:
3. 大小可变性不同
char arr[10]的大小在定义时就固定死了。你无法让它自动变长,装多了就会内存溢出(越界)。std::string是动态扩容的。你随时可以push_back或+=,它内部会自动去申请更大的内存。
4. 安全性不同
char arr[10]; arr[20] = 'a';编译器不会报错,但程序运行会崩溃或产生极其隐蔽的 Bug(这叫缓冲区溢出)。string str; str[20] = 'a';如果你用str.at(20)会直接抛出异常报错;如果用str[20],虽然不报错,但通常也是越界,不过string本身会帮你管理内存,不容易造成灾难性崩溃。
💡 形象比喻
char数组:就像一块划好格子的水泥地。大小固定,你只能往里放东西,不能直接把另一块地“赋值”给它。想复制内容,必须用strcpy一个个搬。std::string:就像一个带有智能收纳功能的箱子。它自带标签(记录长度),能自动变大变小,你可以直接把另一个箱子里的东西倒进来(=),还能随意在中间掏个洞(substr)。
🎯 初学者的实战建议
- 日常写题、写项目,无脑用
std::string。它安全、方便、功能强大。 - 什么时候必须用
char[]? 只有当你需要调用一些老旧的 C 语言 API(比如fopen打开文件),或者嵌入式开发极度抠内存时。 - 不要写
char arr[n] = str;。如果非要把string转成char数组,正确的做法是:
把 std::string 当成一个完整的对象,而不是单纯的数组。记住这一点,你以后就不会再踩 char 数组的那些坑了!
char 数组与 std::string 对比表
| 特性 | char 数组(C 风格字符串) | std::string |
|---|---|---|
| 本质 | 就是一块 char 内存,以 '\0' 结尾 | 一个类,封装了字符缓冲区 |
| 头文件 | <cstring>(函数) |
<string> |
| 长度 | 不知道,靠 '\0' 判断结尾 | 内部存了长度,.size() O(1) |
| 内存管理 | 手动,固定大小,可能溢出 | 自动扩容,不用操心 |
| 赋值 | 不能直接 =,要 strcpy |
直接 =,+ 拼接 |
| 比较 | strcmp |
直接 ==、<、> |
| 安全性 | 容易越界、溢出 | 相对安全 |
| 是否对象 | 不是,是原始内存 | 是,有成员函数 |
一句话:char 数组是”裸内存 + 约定”,std::string 是”封装好的字符串类”。
指针
赋值操作总结表
| 写法 | 是否合法 | 含义 | 说明 |
|---|---|---|---|
p = &a; |
✅ | 让 p 指向 a | 赋变量地址,最常用 |
p = q; |
✅ | 让 p 指向 q 指向的地方 | 指针互相赋值,共享地址 |
p = nullptr; |
✅ | 让 p 不指向任何地方 | 空指针,安全 |
p = arr; |
✅ | 数组名退化为首地址 | 数组名本身就是首地址 |
p = new int(5); |
✅ | 动态内存 | 记得用 delete 释放 |
p = 10; |
❌ | 不能把值赋给指针 | 语法错误,类型不匹配 |
int* p = pd;(类型不同) |
❌ | 类型必须匹配 | 两个指针类型不同 |
p = (int*)10; |
⚠️ | 语法可过,极度危险 | 强制转换不安全 |
*p = 100; |
✅ | 把 p 指向的变量改成 100 | 看等号左边有没有 *,有就是改“指向的内容”,没就是改“指针自己” |
*p = &a; |
❌ | *p 是 int,不能赋地址 | 类型不匹配 |
int* p = &a; |
✅ | a 的地址赋给指针 p | int* ← int* |
定义时:int* p = &a; —— 可以拿地址初始化指针 非定义时:p = &a; —— 不行,p 已经是它指向的那个变量了,只能给它赋值 区别就在于 前面有没有 int: int p → p 是指针,可以接地址 *p → p 指向的那个变量,只能接值
核心:看等号左边有没有*有, * 就是改“指向的内容”,没 * 就是改“指针自己”
指针安全警告
| 操作 | 是否合法 | 说明 |
|---|---|---|
| 使用未初始化的指针 | ❌ | 未定义行为,直接用会崩溃 |
| 使用已释放/越界的指针 | ❌ | 未定义行为,会破坏内存 |
| 越界指针 | ❌ | 指针跑到数组外面,写进去会破坏别的内存 |
| 悬空指针 | ❌ | 指向的内存已经被释放,写进去会破坏已归还的内存 |
共同点:指针看起来”还有效”,其实已经无效,C++语法不会提醒你。
函数内无法用 sizeof 得到数组长度,在函数内部,sizeof(arr) 拿不到数组长度,因为数组作为参数传进来时已经退化成指针了,必须额外传 size