news 2026/10/15 21:42:10

C-C++之string和char的使用及区别

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C-C++之string和char的使用及区别

前言


初学 C/C++ 的人几乎都会经历同一个困惑:一会儿写char s[] = "abc";,一会儿写char* p = "abc";,一会儿又写std::string s = "abc";——三者看起来都在"存字符串",但行为、生命周期、可否修改、能不能相互赋值,处处不同。更麻烦的是,C 与 C++ 混用时,编译器给出的报错往往只能意会不能言传,比如const char*无法转换为char*、sizeof出来的值跟想象的字符数不一样。


这背后的核心矛盾其实只有一句话:C 语言的字符串是"以'\0'结尾的char数组"这一约定,不是一种类型;而 C++ 的std::string是一个真正管理内存的类。理解了这句话,剩下的全是细节推论。本文从char本身讲起,再讲 C 风格字符串的三种形态,最后讲std::string,并给出互转、传参、以及真实踩坑的完整示例。


一、char 到底是什么:一个"最小的整数"


char首先是一个整数类型,大小恒为 1 字节(sizeof(char) == 1,这是标准规定)。它能否表示负数取决于实现:




类型取值范围(典型)说明



char-128 ~ 127或0 ~ 255由实现定义,x86 上多为有符号

signed char-128 ~ 127明确有符号

unsigned char0 ~ 255明确无符号



这一点带来的第一个实用结论是:凡是把char当作"字节值"参与算术或下标运算(如哈希、位图、UTF-8 解析),一律用unsigned char。一个字节0xE4(UTF-8 汉字首字节)作为有符号char是-28,直接用作数组下标就会越界。


#include <stdio.h>
int main(void) {
char c = (char)0xE4;
unsigned char u = 0xE4;
printf("char=%d, unsigned char=%u, sizeof(char)=%zu\n",
(int)c, (unsigned)u, sizeof(char)); // char=-28, unsigned char=228, 1
return 0;
}

字符串字面量"abc"的类型也不是char*,而是const char[4](含结尾'\0')。它在 C 中被隐式转换为char*仅为兼容历史,修改它属于未定义行为;在 C++ 中则根本不允许把const char*赋给char*。


二、C 风格字符串的三种形态




写法存储位置可修改生命周期



char a[] = "abc";栈(数组,拷贝一份)✅ 可改随作用域结束

char* p = "abc";字面量在只读段(rodata)❌ 改了崩溃程序全程有效

char* p = malloc(n)堆✅ 可改到free为止

static char a[] = "abc";静态数据区✅ 可改程序全程有效



关键区别在于数组会拷贝,指针只指向字面量:


#include <stdio.h>

void demo(void) {
char a[] = "hello"; // 栈上 6 字节,内容是 'h','e','l','l','o','\0'
char* p = "hello"; // p 指向只读段的 6 字节

a[0] = 'H'; // ✅ 合法,输出 Hello
// p[0] = 'H'; // ❌ 未定义行为,多数平台直接段错误
printf("%s %s\n", a, p);
}

sizeof在这里也是坑:sizeof(a)是数组大小6,sizeof(p)是指针大小8(64 位),与字符串长度无关。求长度必须用strlen。


char a[] = "hello";
char* p = a;
printf("%zu %zu %zu\n", sizeof(a), sizeof(p), strlen(a)); // 6 8 5

三、std::string 是什么


std::string是std::basic_string<char>的别名,是一个管理动态内存的类:



  • 内部维护"数据指针 + 长度 + 容量",字符串内容存在堆上(短字符串可能被 SSO 优化存进对象内部,见后文坑点)。

  • 自动管理内存:拷贝构造 / 移动构造 / 析构都由类完成,不需要free。

  • 提供长度size()、拼接+、比较==、子串substr()等成员,语义都是"值语义"(拷贝得到独立副本)。

  • 保证data()返回的缓冲区以'\0'结尾(C++11 起),因此可安全传给 C API。




维度char[]/char*std::string



类型本质数组或指针(无长度信息)类,内含长度与容量

长度获取strlen遍历 O(n)size()直接返回 O(1)

拼接strcat,需自行保证容量+/append,自动扩容

比较strcmp,返回三态==/<,返回 bool 或字典序

内存管理手动malloc/free自动(RAII)

越界安全无保护,直接 UBat()会抛异常,[]仍无保护

可存'\0'否(会截断)✅ 可以,size()才准

二进制安全❌✅



最后两行是std::string相对 C 风格字符串的质的优势:std::string可以保存中间含'\0'的二进制数据,长度以size()为准;而 C 风格字符串一旦中间有'\0',后面全部丢失。


四、互转:三条路


#include <string>
#include <cstring>
#include <vector>

void convert() {
// C 风格 -> std::string:直接构造(会拷贝)
const char* c = "hello";
std::string s1(c);
std::string s2(c, 3); // 只取前 3 字节 "hel"
std::string s3 = c; // 拷贝赋值

// std::string -> C 风格:c_str() / data() 返回 const char*
const char* p = s1.c_str(); // 只读,勿修改
const char* q = s1.data(); // C++11 起同样以 '\0' 结尾

// 需要可写缓冲区时,不能用 c_str(),要自己拷一份
std::vector<char> buf(s1.begin(), s1.end());
buf.push_back('\0'); // 别忘了结尾
// 或者(C++17):
// std::string tmp = s1; tmp.data()[0] = 'H'; // data() 返回可写指针(C++17 起非 const 重载)
}

要点:c_str()返回的指针指向std::string内部缓冲区,任何导致扩容的操作都会让它失效;也不能通过它修改内容(是const char*)。想改,就自己拷贝到std::vector<char>或新的std::string。


五、代码实战:一个跨 C/C++ 的字符串处理示例


下面这份代码可直接编译,演示了长度计算、拼接、比较、互转以及常见错误的正确写法。


// 编译:g++ -std=c++17 -O2 -Wall -Wextra demo_char_string.cpp -o demo
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>

// 把 C 风格字符串按给定分隔符切分,返回 std::string 版本(二分查找更好,此处演示)
void toUpperC(char* s) { // 参数是"可写"的,调用者保证
for (; *s; ++s) {
if (*s >= 'a' && *s <= 'z') *s = static_cast<char>(*s - 'a' + 'A');
}
}

int main() {
// 1) 长度:sizeof 与 strlen 的区别
char arr[] = "abc";
char* ptr = arr;
std::printf("sizeof(arr)=%zu strlen=%zu sizeof(ptr)=%zu\n",
sizeof(arr), std::strlen(arr), sizeof(ptr)); // 4 3 8

// 2) 数组可改,字面量不可改
toUpperC(arr);
std::printf("after toUpperC: %s\n", arr); // ABC

// 3) std::string 拼接收尾无需关心 '\0'
std::string s = "Hello";
s += ", ";
s += "World";
s.push_back('!');
std::cout << s << " len=" << s.size() << '\n'; // Hello, World! len=13

// 4) 互转
const char* cs = s.c_str(); // string -> C,只读
std::string s2(cs); // C -> string,拷贝
std::cout << std::strlen(cs) << ' ' << s2.size() << '\n'; // 13 13

// 5) 二进制安全:std::string 可以存 '\0'
std::string bin("a\0b", 3);
std::cout << "bin.size()=" << bin.size() << '\n'; // 3
std::printf("C strlen=%zu\n", std::strlen(bin.c_str())); // 1(被 '\0' 截断)

// 6) 比较语义不同
std::printf("strcmp=%d\n", std::strcmp("abc", "abd")); // 负数(<0)
std::cout << ("abc" < std::string("abd")) << '\n'; // 1(bool 真)
return 0;
}

常见坑点


坑 1:把c_str()的指针长期保存


❌ 错误写法:


std::string s = "hello";
const char* p = s.c_str();
s += " world, this is a very long string that forces reallocation";
printf("%s\n", p); // p 可能已失效 → 输出乱码或崩溃

✅ 正确写法:


std::string s = "hello";
s += " world, ...";
const char* p = s.c_str(); // 内容定了再取指针
printf("%s\n", p);

c_str()/data()返回的指针在字符串扩容或修改后即失效。若确需长期持有,请拷贝成std::string或std::vector<char>。


坑 2:修改字符串字面量


❌ 错误写法:


char* p = "hello";
p[0] = 'H'; // 未定义行为,Linux 上段错误,部分嵌入式平台"居然能过"

✅ 正确写法:


char p[] = "hello"; // 拷贝到栈上,可改
p[0] = 'H';
// 或者
const char* q = "hello"; // 明确只读,编译器会帮你挡

坑 3:缓冲区溢出 ——strcpy/sprintf不检查长度


❌ 错误写法:


char buf[8];
strcpy(buf, userInput); // 只要输入超过 7 字节就溢出
sprintf(buf, "%s-%d", name, id);

✅ 正确写法:


char buf[8];
snprintf(buf, sizeof(buf), "%s", userInput); // C99 起,自动截断并保证 '\0'
// 或使用 strncpy 并手动补 '\0'(注意 strncpy 不保证结尾)
strncpy(buf, userInput, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

缓冲区溢出(buffer overflow)是 C 代码最经典的安全漏洞,栈上溢出可被利用来劫持返回地址。新代码应优先使用std::string或std::span(C++20)替代裸数组。


坑 4:返回局部数组的指针


❌ 错误写法:


char* getName(void) {
char buf[32] = "tom";
return buf; // buf 在函数返回后即销毁 → 悬垂指针
}

✅ 正确写法:


// 方案 A:调用者提供缓冲区
void getName(char* out, size_t n) { snprintf(out, n, "%s", "tom"); }

// 方案 B(C++):直接返回 std::string,由返回值优化/移动语义接管
std::string getName() { return "tom"; }

坑 5:strncpy不保证以'\0'结尾


char buf[4];
strncpy(buf, "hello", sizeof(buf)); // buf = {'h','e','l','l'},无 '\0'
printf("%s\n", buf); // ❌ 越界读取,直到偶然遇到 '\0'

strncpy(dst, src, n)在strlen(src) >= n时不会补'\0'。要么手动补,要么用snprintf。


坑 6:SSO 导致c_str()地址"看起来"稳定


主流标准库对小字符串有短字符串优化(SSO, Small String Optimization):长度不超过 15(libstdc++)或 15(MSVC)的字符串直接存在对象内部的栈缓冲区里,不分配堆内存。这会造成一种假象——"我c_str()之后改字符串,指针内容没变"。一旦字符串变长触发堆分配,同样的代码立刻崩溃。不要依赖 SSO 行为写代码,它不在标准里,且各实现阈值不同。


坑 7:std::string与'\0'的长度陷阱


std::string s("a\0b", 3); // size()==3
const char* p = s.c_str();
printf("%s\n", p); // 输出 a(被第一个 '\0' 截断)
printf("%zu\n", strlen(p)); // 1,不是 3

传给 C API 时会"丢数据"。处理二进制数据时,务必同时传长度,不要只传指针。


总结




场景推荐做法原因



需要可修改的字符串常量char a[] = "...";栈上拷贝,可写

只读字符串字面量const char* p = "...";明确只读,避免 UB

一般业务字符串std::string自动内存管理、O(1) 长度、可存'\0'

传给 C APIs.c_str()保证'\0'结尾(勿长期持有)

接收 C API 写回std::vector<char>可写、可自行控制长度

字节/哈希运算unsigned char避免负下标与符号扩展

格式化输出snprintf有长度上限,不溢出



一句话收尾:C 的字符串是"约定",C++ 的std::string是"类型"。约定需要你手工维护(长度、结尾、容量),类型则由编译器与标准库替你维护。能用类型的地方就不要用约定,这是从 C 走向 C++ 最重要的一步。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 21:41:33

CMake构建使用实战:跨平台C++工程的配置、依赖与调试

最早被 CMake 折腾到半夜&#xff0c;是在一个跨平台 C 工具的构建迁移上。那时项目同时要在 Windows 和 Linux 上出包&#xff0c;手写 Makefile 加 Visual Studio 工程文件&#xff0c;每次新增一个源文件都得改两处&#xff0c;外面引用的第三方库还各有各的编译方式&#x…

作者头像 李华
网站建设 2026/10/15 21:41:07

第六章:CodeCave扫描与.text节空隙利用

大家好&#xff0c;你们可以叫我凌。 今天我们来唠唠 Code Cave 注入&#xff0c;不过该注入方法常常因为CFG而经常无法成功&#xff0c;所以说这篇文章仅仅用于讲解其中原理。 当然&#xff0c;要想注入成功则需要用 SetProcessValidCallTargets 把 Code Cave 地址注册为合法…

作者头像 李华
网站建设 2026/10/15 21:40:53

余弦相似度与欧氏距离:相似度度量的核心差异与工程选择

很多人在刚接触机器学习或者做推荐系统、文本匹配的时候&#xff0c;都会遇到两个绕不开的名字&#xff1a;余弦相似度和欧氏距离。这俩确实是衡量"东西像不像"最常用的两个指标&#xff0c;但也是被误解得最深的一对。我见过太多人拿到数据就无脑算一个&#xff0c;…

作者头像 李华
网站建设 2026/10/15 21:40:51

从零搭建聚合搜索工具:搜索源接入、结果聚合与部署实践

最近在整理书签和常用工具时&#xff0c;发现“柠檬搜索”这类聚合搜索工具又回到了开发者的视野里。这次回归不仅带来了更清爽的界面&#xff0c;也让大家重新开始讨论“自建搜索入口”这件事到底值不值得做。与其停留在“好不好用”的层面&#xff0c;不如直接动手&#xff0…

作者头像 李华
网站建设 2026/10/15 21:38:05

Agent落地最后一公里:灵衢互联、分级适配与软硬协同实践解析

这段时间被Agent折腾得够呛的朋友应该不少&#xff1a;模型聪明不聪明已经排不到第一位了&#xff0c;真正让人头大的是&#xff0c;Agent在演示环境里跑得行云流水&#xff0c;一旦搬到生产环境就各种“最后一公里”问题冒出来——工具调用超时、端侧算力吃不消、多个Agent互相…

作者头像 李华
网站建设 2026/10/15 21:36:24

近世代数课件怎么啃?从代数系统主线到运算性质一次讲透

简介&#xff1a;近世代数课件面向数学专业本科生、考研学生以及自学抽象代数的读者&#xff0c;系统梳理了近世代数的核心概念与知识框架。内容从代数系统的定义与性质切入&#xff0c;依次介绍同构与同态、同余关系、商代数和积代数、半群与独异点、群与子群、循环群与置换群…

作者头像 李华