C++第五次:malloc /calloc/new /free/delete/auto/decltype /范围for循环/编译过程/typedef/using/字符串/转义字符总结
一、malloc /calloc/new /free/delete 完整总结函数 / 关键字所属语言依赖头文件用途定位mallocC 语言stdlib.h堆内存原始分配只分配裸内存不初始化callocC 语言stdlib.h堆内存分配分配后自动清零整块内存freeC 语言stdlib.h释放malloc/calloc分配的堆内存newC 关键字无需头文件分配内存 调用构造函数初始化对象支持基础类型 / 自定义类deleteC 关键字无需头文件释放new内存 调用析构函数清理对象资源1. mallocint* p (int*)malloc(n * sizeof(int)); if (p NULL) { // 错误处理 } // 自己赋值 // 用完释放 free(p); p NULL;参数需要分配的总字节数行为在堆上开辟size字节连续内存不会初始化内存内存里是随机脏值分配失败返回NULL必须判空为什么 malloc 不初始化为了性能。初始化需要额外遍历内存很多场景不需要初始值因此不做。2. callocvoid* calloc(size_t num, size_t size);参数num元素个数size单个元素字节大小总内存 num * size行为分配完成后整块内存全部置 0分配失败返回NULL等价效果malloc分配后手动memset清零// 10个int全部初始化为 0 int* p (int*)calloc(10, sizeof(int)); int* p (int*)calloc(n, sizeof(int)); // 自动清0calloc 为什么适合数组因为数组默认需要 0 初始化calloc 一步到位不用手动 memset。补充memsetmemset(指针, 0, 大小); // 99% 的用途就是清0 // 把 p 指向的内存全部设为 0 memset(p, 0, 10 * sizeof(int));memset(p,0,count*size):把p指向的地址清零memset(p,0,count*size):把p清零freep释放p指向地址的空间freep释放p指针函数作用初始化底层性能malloc分配指定字节内存不初始化脏数据空闲链表 sbrk/mmap最快calloc分配数组内存自动全部清零malloc memset较慢memset填充内存手动设置每个字节逐字节 / 批量赋值中3. freevoid free(void* ptr);只用于释放malloc / calloc开辟的堆内存不能释放栈内存、new分配的内存释放后建议手动置空ptr NULL避免野指针重复 free、free 栈变量会触发程序崩溃。内存释放free底层逻辑free 不会立即把内存还给操作系统而是标记内存块为空闲合并相邻空闲块防止内存碎片放入空闲链表供下次 malloc/calloc 复用4. newC分两种用法单对象、数组// 单个对象 int* p1 new int; // 分配内存不初始化 int* p2 new int(10); // 分配 初始值10 // 数组 int* arr new int[10]; // 分配10个int随机脏值 int* arr2 new int[10]{}; // C11起数组全部清零 // 自定义类自动调用构造函数 class Student{}; Student* s new Student();核心特性自动计算类型字节不用手动算sizeof自动强类型转换不用强制(int*)分配失败默认抛std::bad_alloc异常可捕获自动调用类的构造函数完成对象初始化C 函数做不到。5. deleteC配套new释放区分单个对象和数组必须配对使用int* p new int(5); delete p; // 释放单个对象 int* arr new int[10]; delete[] arr; // 数组必须加[]否则内存泄漏/崩溃核心特性释放前自动调用类的析构函数释放类内部资源文件、堆内存等不能用来释放malloc/calloc的内存底层堆管理不兼容释放后置空防止野指针。失败处理C判断返回指针是否为 NULLC默认抛出异常可 try-catch 捕获。二、C 动态内存new /delete3 种使用形式2.1.运算符 new分配内存 自动初始化底层执行四步自动算好要开辟的内存大小不用自己写 sizeof底层调用类似 malloc 去堆区申请内存给这块内存做初始化括号 / 大括号赋值返回对应类型指针内存不够申请失败直接抛std::bad_alloc异常不想抛异常std::nothrow写法如果内存不够不抛异常直接返回空指针 NULL方便后续判断指针是否有效int* p new(std::nothrow) int(10); if (p nullptr) { // 内存开辟失败处理逻辑 }new两种开辟形式单个变量空间()int* p new int(10); // 堆上开1个int直接赋值10 // 等价简写 int* p new int{10};2.数组空间[]int* s new int[10]{1,2,3,4,5,6,7,8,9,10}; // 堆上连续开10个int批量初始化数组申请一个int空间【】申请一组int空间释放空间释放之后一定要制空2.2. operator new 函数用法只能申请空间无法对空间初始化长得像函数只负责分配原始堆内存不会初始化内存内容和 malloc 行为几乎一样只有一点区别operator new内存不足抛std::bad_alloc异常malloc内存不足直接返回 NULL// 手动自己算sizeof强制类型转换和malloc格式几乎一致 int* p (int*)::operator new(sizeof(int)); // 释放对应用 operator delete operator delete(p); p nullptr; // 释放后置空函数用法和malloc用法一致。唯一区别在于new失败抛出异常malloc返回空2.3. 定位 new/构建new核心特点不向堆申请新内存直接在一块已经存在的内存栈 / 堆都行上调用构造函数做初始化 适用场景提前预留一块内存后续再按需创建对象 示例int main() { char buf[20]; // 栈上提前开好20字节内存 new (buf) int(10); // 在buf起始位置放int 10 new (buf 4) int(20); // 偏移4字节一个int大小再放int 20 return 0; }注意定位 new 开辟的对象不能直接用 delete 释放需要手动调用析构函数再回收底层内存2.4.new和malloc的区别2.5.free和delect的区别1. delete 配套 newC 专用单个对象用delete 指针;new [] 开辟的数组必须用delete[] 指针;写错会内存错乱 / 崩溃int* p new int(10); delete p; p nullptr; // 释放后一定要置空防止野指针 int* s new int[10]{1,2,3}; delete[] s; s nullptr;规范内存释放后立刻把指针赋值 nullptr杜绝野指针问题2. free 配套 mallocC 语言风格int* tmp (int*)malloc(sizeof(int)*5); free(tmp); tmp nullptr;三、C11 auto 自动类型推导3.1. 基础语法规则硬性要求auto 定义变量必须初始化编译器靠等号右边的值推导出变量真实类型无初始值直接编译报错auto a 10; // a → int auto dx 12.25; // dx → double auto ft 12.25f;// ft → float auto pa a; // pa → int* auto pd dx; // pd → double*补充拓展基础写法auto b 3.14; // double auto c x; // char auto str abc; // const char*auto 简化复杂类型核心优势迭代器场景容器迭代器原生类型极长auto 直接省略#include vector #include iostream using namespace std; int main() { vectorint vec {1,2,3}; // 原生超长写法vectorint::iterator it vec.begin(); auto it vec.begin(); // 编译器自动推导迭代器类型 cout *it endl; return 0; }3.2. 同一行定义多个 auto 变量 考题int main() { auto x 10; auto p x, z 10; // 写法1编译报错 auto *p x, z 10; // 写法2编译通过 }原理同一行 auto 推导的基础类型必须统一 写法 1 中p推导为int*、z推导为int两种基础类型冲突报错 写法 2 显式标注auto *pauto 只推导基类型intz同样推导为 int类型统一合法。3.3. auto 推导完整核心规则必记包含 const 常量推导规则规则 1裸 auto 默认忽略顶层 const、引用// 丢弃顶层const const int x 10; auto a x; // a 的类型是 int不是 const int顶层const被丢弃a可修改 // 丢弃引用属性 int num 20; int ref num; auto b ref; // b 的类型是 int不是 int引用属性直接丢掉const 常量拓展示例const int a 10; auto x a; // x是普通int顶层const丢失x可修改 auto ra a; // ra是const int绑定常量自动带上constra不可修改 auto p a; // p是const int*指针指向常量*p不可修改但对于引用和指针auto推演出来的类型就是带const因为它们是a的别名和指向这是不可以修改的规则 2想要保留引用 → autoint num 20; auto ref_num num; // ref_num 类型 int和num绑定修改ref_num会直接修改原变量 ref_num 99;规则 3想要保留 const 常量 → const autoconst int x 100; const auto val x; // val 是 const int不能修改规则 4同时保留 const 引用 → const auto最常用只读不拷贝string s hello; // 推导为 const string不拷贝字符串、只读运行效率高 const auto temp s;规则 5auto 配合指针两种写法等价int m 5; auto p m; // p 是 int* auto* p2 m; // 和上面等价显式写*可读性更好4. auto 不能使用的场景坑点编译全报错变量不初始化auto x; // 错误无初始值无法推导类型2.C11 函数形参不能直接写 autoC14 才支持泛型 auto 参数// C11直接报错 void func(auto x) {}C11 类内成员变量不允许 autoC17 放开限制class Test { auto a 10; // C11编译报错 };不能直接定义 auto 数组auto arr[5] {1,2,3}; // 语法错误四、decltype 关键字弥补 auto 必须初始化的缺陷4.1. decltype 基础概念auto 修饰的变量必须初始化编译器依靠初始值确定类型 如果只需要提取表达式类型、不需要立刻定义变量使用decltype C11 新增decltype(exp)编译期推导表达式类型不会执行表达式计算仅提取类型。基础语法示例int a 10, b 20; double dx 12.23; decltype(a b) x 0; // 仅推导ab的类型int不会实际执行ab的加法运算但针对a)和a又有了不一样的情况这个需要记死4.2. decltype 推导引用特殊规则decltype(a)前置自增返回变量左值本体a本身推导为int引用必须初始化decltype(a) y; // 报错int引用无初始值当为a的时候我们认为他返回的是a本身即y不是一个整形而是一个整形引用类型引用是必须要初始化的2.decltype(a)后置自增返回临时右值推导普通int可无初始值定义decltype(a) z; // 合法普通int类型当为a的时候z是一个整形不是一个引用3.decltype((变量))变量外层包裹双层括号强制推导为引用类型int x 5; decltype((x)) ra x; // ra是int引用4.3. decltype (auto) 拓展区分普通 auto普通 auto 会丢弃 const、引用属性decltype(auto)会完整保留表达式原本的 const、引用类型int n 10; int r n; auto a r; // int丢失引用 decltype(auto) b r; // int完整保留引用属性五、C11 基于范围的 for 循环范围 for死循环的三种写法for的效率最高1. 在c中遍历一个数组容器的方法一般如下int main() { int ar[] {1,2,3,4,5,6,7,8,9}; int n sizeof(ar) / sizeof(ar[0]); // 下标遍历 for(int i 0; i n; i) { cout ar[i] ; } coutendl; // 指针遍历 int* ip NULL; for(ip ar; ip ! ar n; ip) { cout *ip ; } coutendl; return 0; }2. 范围 for 核心语法限制格式for(类型 临时变量 : 数组/容器)限制冒号后只能填写完整数组、容器对象不支持裸指针考题示例(谁可以运行int main() { int ar[10]{1,2,3,4,5,6,7,8,9,10}; int *ip ar; int (br)[10] ar; // br是数组引用保留完整数组类型信息 for(auto x : ip)// ❌ 编译报错ip是int*裸指针只保存地址忽略了本身的长度 { } for(auto x : br) // ✅ 正常运行br是合法数组引用 { } return 0; }范围 for 搭配 auto 三种写法完整区分int() { int ar[] {12,23,34,45,56,67}; // 1. auto x : 数组值拷贝操作副本修改x不会影响原数组 for(auto x : ar) { cout x ; } coutendl; // 2. auto x : 数组绑定原元素引用修改x会直接改动原数组内容 for(auto x : ar) { cout x ; } coutxendl; // 3. const auto x : 数组只读引用无拷贝开销遍历性能最优只读场景首选 for(const auto x : ar) { cout x ; } coutendl; return 0; }六、编译阶段1. C 程序编译四阶段流程预处理 → 编译 → 汇编 → 链接 文件转换链路yhp.cpp→预处理→yhp.i→编译→yhp.s→汇编→yhp.o(yhp.obj)→链接→yhp.exe2.用nullptr代替NULL七、类型重命名typedef和using1. 基础等价写法数组类型别名// C语言老式typedef写法 typedef int Array[10]; // C11 using新式写法更直观推荐 using Array int[10];两者完全等价都表示Array是长度为 10 的 int 数组类型。核心区别using支持模板别名typedef做不到typedef无法结合模板using可以定义模板类型别名多线程 / 泛型高频使用// 模板数组别名任意类型T、任意长度N的数组 templateclass T, size_t N using Array T[N]; // 使用创建长度5的double数组 Arraydouble,5 d_arr {1.1,2.2,3.3,4.4,5.5};八、字符串相关知识includestring和string.h有巨大的差异string.h和cstring是一个东西1. 头文件区分string.hC 语言原生字符串头文件提供strlen/strcpy等 C 风格字符数组函数cstringC 对string.h的封装功能完全一致命名空间为stdstringC 标准字符串容器头文件专属std::string和上面两个完全无关。2.sizeofvsstrlensizeof(变量)运算符计算整个变量占用内存字节数包含末尾\0数组char c[] abc; sizeof(c)43 字符 1 结束符指针char* p abc; sizeof(p)8(x64)/4(x86)只算指针本身大小和字符串长度无关指针在x86下占4字节在x64下占8个字节strlen(字符指针)C 库函数只计算有效字符长度遇到\0立刻停止不计入\0strlen(abc) 33.std::stringC 第一个标准容器底层封装字符数组自带长度管理、内存自动扩容不用手动处理\0。四种初始化写法#include string // 1. 空字符串长度0 std::string stra; // 2. 等号列表初始化 std::string strb {yhpinghello}; // 3. 构造函数初始化 std::string strc(yhpinghello); // 4. C11列表初始化推荐 std::string strd{yhpinghello};4. string 两种下标遍历方式[]vs.at()std::string strd{yhpinghello}; // 方式1[]下标访问 for (int i 0; i strd.size(); i) { std::cout strd[i] std::endl; } // 方式2.at()访问 for (int i 0; i strd.size(); i) { std::cout strd.at(i) std::endl; }核心差异笔记补充解释str[i]不做越界检查。如果i str.size()访问非法内存输出乱码 / 程序崩溃速度更快.at(i)内置越界校验。下标超出范围会抛出std::out_of_range异常方便调试安全性更高。5. 字符串合并运算符重载std::string重载了可以直接拼接两个字符串对象std::string stra yhping; std::string strb hello; std::string strd stra strb; std::cout strd std::endl; // 输出 yhpinghello注意不能直接写ab两个字面量是const char*不支持拼接至少一侧是std::string才行。九、编译器内置宏调试打印专用无需定义编译器自动提供快速获取代码位置信息#include iostream int main() { // 当前代码文件完整路径字符串 std::cout __FILE__ std::endl; // 当前所在函数名 std::cout __func__ std::endl; // 当前代码行号数字 std::cout __LINE__ std::endl; // 程序编译时间 std::cout __TIME__ std::endl; // 程序编译日期 std::cout __DATE__ std::endl; return 0; }实用场景日志打印快速定位报错代码位置。十、转义字符单引号 / 双引号 / 反斜杠打印基础规则字符串 / 字符字面量中 \拥有特殊语法含义想要原样打印必须加反斜杠\转义。单引号字符字面量 的边界符号内部写\代表单引号字符双引号字符串字面量 的边界符号内部写\代表双引号字符反斜杠\转义标记想要输出\必须写两个\\。/转义字符‘/’才能被识别为单引号字符如果不带/会被认为是一个字符的开始“/”双引号字符如果不带/会被认为是字符串的开始代码示例// 存储字符单引号 char chb \; // 存储字符双引号 char chc \; // 存储字符反斜杠 \ char chd \\;拓展打印演示std::cout chb chc chd; // 输出 \int*裸指针无法使用范围 forstd::string是容器支持for(auto c : strd)简洁遍历typedef只支持基础类型别名模板别名一律用using区分 C 风格char[]和 Cstd::string前者需要手动strlen、防越界后者自带.size()和安全at()空指针统一用nullptr抛弃NULL避免重载 bug转义字符只在引号包裹的字面量里生效变量输出不需要转义。