C++第五次:malloc /calloc/new /free/delete/auto/decltype /范围for循环/编译过程/typedef/using/字符串/转义字符总结
一、malloc /calloc/new /free/delete 完整总结
| 函数 / 关键字 | 所属语言 | 依赖头文件 | 用途定位 |
|---|---|---|---|
malloc | C 语言 | <stdlib.h> | 堆内存原始分配,只分配裸内存,不初始化 |
calloc | C 语言 | <stdlib.h> | 堆内存分配,分配后自动清零整块内存 |
free | C 语言 | <stdlib.h> | 释放malloc/calloc分配的堆内存 |
new | C++ 关键字 | 无需头文件 | 分配内存 +调用构造函数初始化对象,支持基础类型 / 自定义类 |
delete | C++ 关键字 | 无需头文件 | 释放new内存 +调用析构函数清理对象资源 |
1. malloc
int* p = (int*)malloc(n * sizeof(int)); if (p == NULL) { // 错误处理 } // 自己赋值 // 用完释放 free(p); p = NULL;- 参数:需要分配的总字节数
- 行为:
- 在堆上开辟
size字节连续内存; - 不会初始化内存,内存里是随机脏值;
- 分配失败返回
NULL,必须判空;
- 在堆上开辟
为什么 malloc 不初始化?
为了性能。初始化需要额外遍历内存,很多场景不需要初始值,因此不做。
2. calloc
void* 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。
补充:memset
memset(指针, 0, 大小); // 99% 的用途就是清0 // 把 p 指向的内存全部设为 0 memset(p, 0, 10 * sizeof(int));memset(p,0,count*size):把p指向的地址清零
memset(&p,0,count*size):把p清零
free(p):释放p指向地址的空间
free(&p):释放p指针
| 函数 | 作用 | 初始化 | 底层 | 性能 |
|---|---|---|---|---|
| malloc | 分配指定字节内存 | 不初始化(脏数据) | 空闲链表 + sbrk/mmap | 最快 |
| calloc | 分配数组内存 | 自动全部清零 | malloc + memset | 较慢 |
| memset | 填充内存 | 手动设置每个字节 | 逐字节 / 批量赋值 | 中 |
3. free
void free(void* ptr);- 只用于释放
malloc / calloc开辟的堆内存; - 不能释放栈内存、
new分配的内存; - 释放后建议手动置空
ptr = NULL,避免野指针; - 重复 free、free 栈变量会触发程序崩溃。
内存释放(free)底层逻辑
free 不会立即把内存还给操作系统,而是:
- 标记内存块为空闲
- 合并相邻空闲块(防止内存碎片)
- 放入空闲链表,供下次 malloc/calloc 复用
4. new(C++)
分两种用法:单对象、数组
// 单个对象 int* p1 = new int; // 分配内存,不初始化 int* p2 = new int(10); // 分配 + 初始值10 // 数组 int* arr = new int[10]; // 分配10个int,随机脏值 int* arr2 = new int[10]{}; // C++11起,数组全部清零 // 自定义类,自动调用构造函数 class Student{}; Student* s = new Student();核心特性:
- 自动计算类型字节,不用手动算
sizeof; - 自动强类型转换,不用强制
(int*); - 分配失败默认抛
std::bad_alloc异常(可捕获); - 自动调用类的构造函数,完成对象初始化(C 函数做不到)。
5. delete(C++)
配套new释放,区分单个对象和数组,必须配对使用:
int* p = new int(5); delete p; // 释放单个对象 int* arr = new int[10]; delete[] arr; // 数组必须加[],否则内存泄漏/崩溃核心特性:
- 释放前自动调用类的析构函数,释放类内部资源(文件、堆内存等);
- 不能用来释放
malloc/calloc的内存,底层堆管理不兼容; - 释放后置空防止野指针。
失败处理
- C:判断返回指针是否为 NULL;
- C++:默认抛出异常,可 try-catch 捕获。
二、C++ 动态内存:new /delete(3 种使用形式)
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 配套 new(C++ 专用)
- 单个对象用
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 配套 malloc(C 语言风格)
int* tmp = (int*)malloc(sizeof(int)*5); free(tmp); tmp = nullptr;三、C++11 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() { vector<int> vec = {1,2,3}; // 原生超长写法:vector<int>::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 *p,auto 只推导基类型int,z同样推导为 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&,绑定常量自动带上const,ra不可修改 auto p = &a; // p是const int*,指针指向常量,*p不可修改但对于引用和指针auto推演出来的类型就是带const,因为它们是a的别名和指向,这是不可以修改的
规则 2:想要保留引用 → auto&
int num = 20; auto& ref_num = num; // ref_num 类型 int&,和num绑定,修改ref_num会直接修改原变量 ref_num = 99;规则 3:想要保留 const 常量 → const auto
const int x = 100; const auto val = x; // val 是 const int,不能修改规则 4:同时保留 const + 引用 → const auto&(最常用,只读不拷贝)
string s = "hello"; // 推导为 const string&,不拷贝字符串、只读,运行效率高 const auto& temp = s;规则 5:auto 配合指针两种写法,等价
int m = 5; auto p = &m; // p 是 int* auto* p2 = &m; // 和上面等价,显式写*可读性更好4. auto 不能使用的场景(坑点,编译全报错)
- 变量不初始化
auto x; // 错误!无初始值,无法推导类型2.C++11 函数形参不能直接写 auto(C++14 才支持泛型 auto 参数)
// C++11直接报错 void func(auto x) {}C++11 类内成员变量不允许 auto(C++17 放开限制)
class Test { auto a = 10; // C++11编译报错 };不能直接定义 auto 数组
auto arr[5] = {1,2,3}; // 语法错误
四、decltype 关键字(弥补 auto 必须初始化的缺陷)
4.1. decltype 基础概念
auto 修饰的变量必须初始化,编译器依靠初始值确定类型; 如果只需要提取表达式类型、不需要立刻定义变量,使用decltype; C++11 新增decltype(exp),编译期推导表达式类型,不会执行表达式计算,仅提取类型。
基础语法示例:
int a = 10, b = 20; double dx = 12.23; decltype(a + b) x = 0; // 仅推导a+b的类型int,不会实际执行a+b的加法运算但针对(++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&,完整保留引用属性五、C++11 基于范围的 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] << " "; } cout<<endl; // 指针遍历 int* ip = NULL; for(ip = ar; ip != ar + n; ++ip) { cout << *ip << " "; } cout<<endl; 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 << " "; } cout<<endl; // 2. auto& x : 数组:绑定原元素引用,修改x会直接改动原数组内容 for(auto& x : ar) { cout << x << " "; } cout<<x<<endl; // 3. const auto& x : 数组:只读引用,无拷贝开销,遍历性能最优(只读场景首选) for(const auto& x : ar) { cout << x << " "; } cout<<endl; return 0; }六、编译阶段
1. C++ 程序编译四阶段
流程:预处理 → 编译 → 汇编 → 链接 文件转换链路:
yhp.cpp→预处理→yhp.i→编译→yhp.s→汇编→yhp.o(yhp.obj)→链接→yhp.exe
2.用nullptr代替NULL
七、类型重命名:typedef和using
1. 基础等价写法(数组类型别名)
// C语言老式typedef写法 typedef int Array[10]; // C++11 using新式写法(更直观,推荐) using Array = int[10];两者完全等价,都表示Array是长度为 10 的 int 数组类型。
核心区别:using支持模板别名(typedef做不到)
typedef无法结合模板,using可以定义模板类型别名,多线程 / 泛型高频使用:
// 模板数组别名:任意类型T、任意长度N的数组 template<class T, size_t N> using Array = T[N]; // 使用:创建长度5的double数组 Array<double,5> d_arr = {1.1,2.2,3.3,4.4,5.5};八、字符串相关知识
include<string>和<string.h>有巨大的差异
<string.h>和<cstring>是一个东西
1. 头文件区分
<string.h>:C 语言原生字符串头文件,提供strlen/strcpy等 C 风格字符数组函数;<cstring>:C++ 对<string.h>的封装,功能完全一致,命名空间为std;<string>:C++ 标准字符串容器头文件,专属std::string,和上面两个完全无关。
2.sizeofvsstrlen
sizeof(变量):运算符,计算整个变量占用内存字节数,包含末尾\0;- 数组:
char c[] = "abc"; sizeof(c)=4(3 字符 + 1 结束符) - 指针:
char* p = "abc"; sizeof(p)=8(x64)/4(x86),只算指针本身大小,和字符串长度无关,指针在x86下占4字节在x64下占8个字节
- 数组:
strlen(字符指针):C 库函数,只计算有效字符长度,遇到\0立刻停止,不计入\0;strlen("abc") = 3
3.std::string:C++ 第一个标准容器
底层封装字符数组,自带长度管理、内存自动扩容,不用手动处理\0。
四种初始化写法
#include <string> // 1. 空字符串,长度0 std::string stra; // 2. 等号列表初始化 std::string strb = {"yhpinghello"}; // 3. 构造函数初始化 std::string strc("yhpinghello"); // 4. C++11列表初始化(推荐) 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注意:不能直接写"a"+"b",两个字面量是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*裸指针无法使用范围 for,std::string是容器,支持for(auto c : strd)简洁遍历;typedef只支持基础类型别名,模板别名一律用using;- 区分 C 风格
char[]和 C++std::string:前者需要手动strlen、防越界,后者自带.size()和安全at(); - 空指针统一用
nullptr,抛弃NULL避免重载 bug; - 转义字符只在引号包裹的字面量里生效,变量输出不需要转义。
