前言
本书介绍的是通用C++,不依赖于特定实现。
第1章 预备知识
- 程序=数据结构+算法
- 面向过程语言采用自上向下方法设计程序,强调的是算法,编程的重点在于用算法解决问题
- OOP采用自下向上的方法设计程序,强调的是数据,编程的重点放在概念表示上
- 泛型编程强调的是独立于特定数据类型
- OOP的概念赋予了C++将问题的多个概念联系起来的能力(高级概念抽象能力),C语言部分的功能赋予了C++充分利用硬件的能力(低级硬件访问能力)
C++11标准长达1350页,夸张……
Bjarne Stroustrup主页: https://www.stroustrup.com/
cin.get()读取输入,直到遇到Enter键结束,加入该语句可以避免出现程序运行后直接退出的情况。
cin.get();
return 0;
第2章 开始学习C++
C++标准的main函数最后如果没有return 0, 默认隐含有这条语句:仅适合于main函数。
#include <iostream>
using namespace std;
int main()
{
cout << "按Enter键继续……"<<endl;
cin.get();
}
- 编译器将启动代码添加到程序中
- main函数被启动代码调用
- main以外的其他函数由main及其他函数调用
<<插入运算符,表示把内容传送给 cout, 指示了信息流动的路径, 是左移运算符<<的重载- cout对象是用于输出内容的对象
- endl 表示换行,是控制符,会立即刷新输出,
\n虽然也可以换行,但不能保证立即刷新输出
重载的例子:
&,地址运算符,AND 运算符*,乘法,对指针解除引用
#include <iostream>
using namespace std;
int main()
{
int carrots;
cout << "你有几个carrots?" << endl;
cin >> carrots;
cin.get();
cout << "我有" << carrots << "个carrots" << endl;
carrots--;
cout << "我吃掉了一个carrot,还有" <<carrots<<"个"<< endl;
cout << "按Enter键继续……" << endl;
cin.get();
return 0;
}
第3章 处理数据
整数
可以考虑只用long 、long long
- long:至少32位:-21亿到21亿
- long long:至少64位
sizeof 函数:https://learn.microsoft.com/zh-cn/cpp/cpp/sizeof-operator?view=msvc-170
#include <iostream>
#include <limits>
using namespace std;
int main()
{
char i_char = CHAR_MAX;
short i_short = SHRT_MAX;
int i_int = INT_MAX;
long i_long = LONG_MAX;
long long i_llong = LLONG_MAX;
cout << "char 类型:" << sizeof(i_char) << "字节," << sizeof(i_char) * 8
<< "位,最大值为" << int(i_char) << endl;
cout << "short 类型:" << sizeof(i_short) << "字节," << sizeof(i_short) * 8
<< "位,最大值为" << i_short << endl;
cout << "int 类型:" << sizeof(i_int) << "字节," << sizeof(i_int) * 8
<< "位,最大值为" << i_int << ",约为" << i_int / 100000000 << "亿"
<< endl;
cout << "long 类型:" << sizeof(i_long) << "字节," << sizeof(i_long) * 8
<< "位,最大值为" << i_long << ",约为" << i_long / 100000000 << "亿"
<< endl;
cout << "long long类型:" << sizeof(i_llong) << "字节,"
<< sizeof(i_llong) * 8 << "位,最大值为" << i_llong << ",约为"
<< i_llong / 100000000 << "亿" << endl;
cin.get();
return 0;
}
运行结果:
char 类型:1字节,8位,最大值为127
short 类型:2字节,16位,最大值为32767
int 类型:4字节,32位,最大值为2147483647,约为21亿
long 类型:4字节,32位,最大值为2147483647,约为21亿
long long类型:8字节,64位,最大值为9223372036854775807,约为92233720368亿
整数字面值
- 十进制数在cout中用dec输出
- 0开头为8进制数,cout中用oct输出
- 0x或0X开头为16进制数,cout中用 hex输出
#include <iostream>
using namespace std;
int main()
{
int i = 0100;
cout << "0100 8进制,10进制,16进制:" << oct << i << " " << dec << i << " "
<< hex << i << endl;
//0100 8进制,10进制,16进制:100 64 40
i = 0X40;
cout << "0X40 8进制,10进制,16进制:" << oct << i << " " << dec << i << " "
<< hex << i << endl;
//0X40 8进制,10进制,16进制:100 64 40
cin.get();
return 0;
}
cout区分输出: char 与 int
#include <iostream>
using namespace std;
int main()
{
int i;
char c;
for (i = 0; i < 127; i++) {
c = i;
cout << i << ":" << c;
if (i != 0 and i % 5 == 0) {
cout << endl;
} else {
cout << " ";
}
}
cin.get();
return 0;
}
输出:
0: 1: 2: 3: 4: 5:
6: 7: 8 9: 10:
14: 15:13:
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:) 42:* 43:+ 44:, 45:-
46:. 47:/ 48:0 49:1 50:2
51:3 52:4 53:5 54:6 55:7
56:8 57:9 58:: 59:; 60:<
61:= 62:> 63:? 64:@ 65:A
66:B 67:C 68:D 69:E 70:F
71:G 72:H 73:I 74:J 75:K
76:L 77:M 78:N 79:O 80:P
81:Q 82:R 83:S 84:T 85:U
86:V 87:W 88:X 89:Y 90:Z
91:[ 92:\ 93:] 94:^ 95:_
96:` 97:a 98:b 99:c 100:d
101:e 102:f 103:g 104:h 105:i
106:j 107:k 108:l 109:m 110:n
111:o 112:p 113:q 114:r 115:s
116:t 117:u 118:v 119:w 120:x
121:y 122:z 123:{ 124:| 125:}
126:~
第4章 复合类型
字符串
- ‘S’ 是83的另一种写法, "S" 是包含’S’ 和 ‘\0’的字符串。
- 输入获取:cin.getline(varName,cacheLen), cacheLen包含了字符串末尾的 ‘\0’
- strlen()不计入 ‘\0’,但计入空格
- cin.getline()遇到换行符
#include "ayzw.top/init.h"
int main() {
//在 C++ 中,当 cin.getline 读取的内容超过设定的长度时,它会设置 failbit 标志位。
// 一旦这个标志位被触发,输入流就会被锁定,后续所有的 cin 输入操作都会被直接跳过。
// 为了防止由于输入超长导致第二行无法输入,必须在每次读取后检测并清除 cin 的错误状态(cin.clear()),并清理缓冲区(cin.ignore())
int8 cacheLen=10;
char name[cacheLen];
cin.getline(name,cacheLen);
cout<<name<<endl;
cout<<"len of name:"<<strlen(name)<<endl;
cin.clear(); // 清除错误标志(解锁 cin)
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // 抛弃缓冲区中剩余的超长字符
cout<<"test again:\n";
cin.getline(name,cacheLen);
cout<<name<<endl;
cout<<"len of name:"<<strlen(name)<<endl;
return 0;
}
输出:
123456789012345
123456789
len of name:9
test again:
12 45
12 45
len of name:5
指针
指针是一个变量,存储的是值的地址,而不是值本身。
1.&运算符
对于常规变量,用取地址符号(&)即可取得它的地址
#include "ayzw.top/init.h"
int main() {
int8 i8=10;
int16 i16=16;
int32 i32=65530;
int64 i64=64LL;
// 补充打印变量地址的语句
cout << "i8 的地址: " << (void*)&i8 << endl; // 必须转成 void*,否则会当成字符串导致乱码
cout << "i16 的地址: " << &i16 << endl;
cout << "i32 的地址: " << &i32 << endl;
cout << "i64 的地址: " << &i64 << endl;
return 0;
}
//i8 的地址: 0x457a7ff6df
//i16 的地址: 0x457a7ff6dc
//i32 的地址: 0x457a7ff6d8
//i64 的地址: 0x457a7ff6d0
指针与C++基本原理:
面向对象编程与传统的过程性编程的区别在于,OOP强调的是在运行阶段(而不是编译阶段)进行决策。运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。运行阶段决策就好比度假时,选择参观哪些景点取决于天气和当时的心情;而编译阶段决策更像不管在什么条件下,都坚持预先设定的日程安排。
运行阶段决策提供了灵活性,可以根据当时的情况进行调整。例如,考虑为数组分配内存的情况。传统的方法是声明一个数组。要在C++中声明数组,必须指定数组的长度。因此,数组长度在程序编译时就设定好了;这就是编译阶段决策。您可能认为,在80%的情况下,一个包含20个元素的数组足够了,但程序有时需要处理200个元素。为了安全起见,使用了一个包含200个元素的数组。这样,程序在大多数情况下都浪费了内存。OOP通过将这样的决策推迟到运行阶段进行,使程序更灵活。在程序运行后,可以这次告诉它只需要20个元素,而还可以下次告诉它需要205个元素。总之,使用OOP时,您可能在运行阶段确定数组的长度。为使用这种方法,语言必须允许在程序运行时创建数组。C++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。
在运行阶段做决策并非OOP独有的,但使用C++编写这样的代码比使用C语言简单。
2.*运算符
*运算符被称为间接值(indirect velue)或解除引用(dereferencing)运算符,将其应用于指针,可以得到该地址处存储的值(这和乘法使用的符号相同;C++根据上下文来确定所指的是乘法还是解除引用)。
例如,假设manly是一个指针,则manly表示的是一个地址,而*manly表示存储在该地址处的值。*manly与常规int变量等效。
#include "ayzw.top/init.h"
int main() {
// 1. 声明并初始化普通整型变量
int64 updates = 6;
// 2. 优化:C++ 更习惯将 * 靠近类型名,并建议在声明时直接初始化
// 这样能一眼看出 p_updates 的类型是「int的指针」
int64 *p_updates = nullptr;
p_updates = &updates;
// -------------------------------------------------------------
// 【补充】如果声明时没有明确的指向,在现代 C++ (C++11及以后) 中
// 必须初始化为 nullptr(空指针),切忌不初始化直接留空。
// 例如:int* p_empty = nullptr;
// -------------------------------------------------------------
// 3. 用两种不同的方式获取数据值
cout << "【数据值】" << endl;
cout << "直接读取变量 updates = " << updates << endl;
cout << "通过指针解引用 *p_updates = " << *p_updates << endl;
cout << "-----------------------------------" << endl;
// 4. 用两种不同的方式获取内存地址
cout << "【内存地址】" << endl;
cout << "对变量取地址 &updates = " << &updates << endl;
cout << "直接打印指针 p_updates = " << p_updates << endl;
cout << "-----------------------------------" << endl;
// 5. 优化:使用更紧凑的复合赋值运算符 += 修改值
*p_updates += 1;
cout << "【修改后的值】" << endl;
cout << "通过指针加 1 后,现在的 updates = " << updates << endl;
return 0;
}
/*
【数据值】
直接读取变量 updates = 6
通过指针解引用 *p_updates = 6
-----------------------------------
【内存地址】
对变量取地址 &updates = 0x37723ffd40
直接打印指针 p_updates = 0x37723ffd40
-----------------------------------
【修改后的值】
通过指针加 1 后,现在的 updates = 7
*/
一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这
是关于使用指针的金科玉律。
使用new分配内存
指针真正的用武之地在于,在运行阶段分配未命名的内存以存储值。在这种情况下,只能通过指针来访问内存。在C语言中,可以用库函数malloc()来分配内存;在C++中仍然可以这样做,但C++还有更好的方法new运算符。
在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。这里的关键所在是C++的new运算符。程序员要告诉new,需要为哪种数据类型分配内存;new将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针,示例:
int* pn = new int;
new int 告诉程序,需要适合存储int 的内存。new运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。接下来,将地址赋给pn,pn是被声明为指向int的指针。现在,pn是地址,而*pn是存储在那里的值。将这种方法与将变量的地址赋给指针进行比较:
int higgens;
int* pt = &higgens;
在这两种情况(pn和pt)下,都是将一个int变量的地址赋给了指针。在第二种情况下,可以通过名称higgens来访问该int,在第一种情况下,则只能通过该指针进行访问。
这引出了一个问题:pn指向的内存没有名称,如何称呼它呢?我们说pn指向一个数据对象,这里的“对象”不是“面向对象编程”中的对象,而是一样“东西”。术语“数据对象”比“变量"更通用,它指的是为数据项分配的内存块。因此,变量也是数据对象,但pn指向的内存不是变量。乍一看,处理数据对象的指针方法可能不太好用,但它使程序在管理内存方面有更大的控制权。
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName* pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。当然,如果已经声明了相应类型的指针,则可以使用该指针,而不用再声明一个新的指针。
new分配的内存块通常与常规变量声明分配的内存块不同。常规变量生命的值都存储在被称为栈(stack)的内存区域中,而new从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。
由于现代 new 失败会抛出异常,通常需要使用 try-catch 块来捕获它
#include <iostream>
#include <new> // 包含 std::nothrow
int main() {
// 使用 std::nothrow,失败时不会抛出异常,而是返回 nullptr (0)
int* p = new (std::nothrow) int[1000000000000ULL];
// 使用 if 语句检查(即你书中提到的方法)
if (p == nullptr) {
std::cout << "内存分配失败,返回了空指针!" << std::endl;
return 1;
}
// 分配成功才可以使用内存
delete[] p;
return 0;
}
在C++中,delete运算符用于将new分配的内存归还给内存池,是防止内存泄漏和实现高效内存管理的关键步骤。
使用delete释放指针指向的内存块后,必须避免重复释放或释放非new分配的内存,且对空指针使用delete是安全的。
#include <iostream>
int main() {
// 1. 标准new和delete配对使用
int* ps = new int; // 分配内存
*ps = 100; // 使用内存
delete ps; // 释放内存
ps = nullptr; // 将指针置空
// 2. 通过另一个指针释放相同内存块
int* ps2 = new int;
int* pq = ps2; // pq指向相同的块
delete pq; // 正确释放
// delete ps2; // 致命错误:禁止二次释放
// 3. 错误示例:禁止释放普通变量内存
int jugs = 5;
int* pi = &jugs;
// delete pi; // 错误:不是new分配的内存
return 0;
}
只能使用delete释放由new申请的内存。
C++ 动态内存分配的核心价值:按需分配与运行时的灵活性。
- 简单变量:如果只需要一个简单的值(如单个 int 或 double),直接声明普通变量更简单、高效。
- 复杂数据:对于大型数据(如大型数组、字符串、结构体),使用 new 才能发挥其最大威力。
- 静态联编(Static Binding)的局限:在编译时就必须确定数组的大小并分配内存。缺点:无论程序运行时最终是否用到该数组,它都会一直占用固定大小的内存,缺乏弹性,容易造成资源浪费(声明太大)或空间不足(声明太小)。
- 动态联编(Dynamic Binding / 动态内存分配)的优势:允许程序在运行时根据用户的输入或实际需求,动态决定是否创建数组以及创建多大的数组。
#include <iostream>
int main() {
// ==========================================
// 1. 小型数据:直接声明简单变量(比 new 更简单高效)
// ==========================================
int age = 18;
std::cout << "简单变量值: " << age << "\n\n";
// ==========================================
// 2. 静态联编(Static Binding):编译时必须确定大小
// ==========================================
// 缺点:即使运行时用户只需要3个空间,这里也死死占用了1000个空间
int staticArray[1000];
std::cout << "静态数组已在编译时分配 1000 个 int 空间。\n";
// ==========================================
// 3. 动态联编(Dynamic Binding):运行时按需分配
// ==========================================
int size = 0;
std::cout << "请输入您实际需要的动态数组大小: ";
std::cin >> size; // 运行时获取用户输入
// 根据运行时的值,动态向系统申请内存
int* dynamicArray = new int[size];
std::cout << "成功在运行时动态创建了大小为 " << size << " 的数组。\n";
// 使用动态数组
for (int i = 0; i < size; ++i) {
dynamicArray[i] = i * 10;
}
// 释放动态数组内存(注意:动态数组释放要用 delete[])
delete[] dynamicArray;
dynamicArray = nullptr;
return 0;
}
//简单变量值: 18
//
//静态数组已在编译时分配 1000 个 int 空间。
//请输入您实际需要的动态数组大小: 100
//成功在运行时动态创建了大小为 100 的数组。
delete[] dynamicArray;用于释放通过 new[] 动态分配的整个数组内存块,并确保数组中的每个元素都能被正确销毁。delete[] dynamicArray;告诉编译器这是数组:当你使用 new int[size] 分配内存时,系统不仅分配了空间,还默默在内存某处记录了该数组的元素数量。精准释放:调用 delete[] 时,编译器会去读取这个数量,从而知道该释放多大的内存。如果是对象数组,它还会根据这个数量调用每个元素的析构函数。- 释放内存后,dynamicArray 指针依然保存着那个内存地址(此时称为野指针 / 悬空指针)。如果后面不小心再次使用它,或者错误地再次 delete 它,程序就会崩溃。将其设为 nullptr 后,后续如果不小心再次 delete nullptr; 是绝对安全的。在实际开发中,执行完 delete[] 后,通常建议紧跟一步置空指针:
delete[] dynamicArray;
dynamicArray = nullptr;
使用new和delete时,应遵守以下规则。
- 不要使用delete来释放不是new分配的内存。
- 不要使用delete释放同一个内存块两次。
- 如果使用new[]为数组分配内存,则应使用delete[]来释放。
- 如果使用new 为一个实体分配内存,则应使用delete(没有方括号)来释放。
- 对空指针应用delete是安全的。对空指针连续 delete 两次依然是绝对安全的,这是因为 C++ 标准严格规定:delete 运算符在执行时会先检查指针的值,如果指针是空指针(nullptr 或 0),delete 将直接跳过,什么都不做。
int main() {
int* p = new int(10);
delete p; // 第一次释放:成功,内存归还系统
p = nullptr; // 关键一步:手动将指针安全地置为空指针
delete p; // 第二次释放:绝对安全!等同于 delete nullptr; 编译器直接忽略
delete p; // 第三次释放:依然绝对安全!
}
动态数组大小
在标准 C++ 中,无法直接通过一个内置函数或运算符(如类似 size() 或 sizeof)来获取由 new[] 分配的动态数组的大小。这是 C++ 底层设计为了追求极致性能而付出的代价:new[] 返回的只是一个纯粹的内存地址指针,它丢失了数组的长度信息。
现代 C++ 标准做法:改用 std::vector,使用标准库的动态数组容器 std::vector。它在底层也是动态分配内存,但它会自己管理大小。
#include <iostream>
#include <vector> // 引入标准库
int main() {
int size = 15;
// 创建一个动态数组,大小为 size
std::vector<int> vec(size);
// 直接调用 .size() 获取大小!
std::cout << "std::vector 的大小是: " << vec.size() << std::endl;
// 自动释放内存,不需要写 delete[]
return 0;
}
模板类 vector 是一种动态数组。它是使用 new 和 delete 手动管理动态数组的现代化、自动化替代品,其底层内存的申请与释放全部由系统自动完成,能够有效避免内存泄漏。
- 动态扩容:可以在运行阶段设置长度,支持在末尾附加新数据或在中间插入新数据。
- 按需初始化:由于长度可自动调整,初始元素数量可以设置为 0。
- 常量/变量均可传参:创建时指定的元素个数,既可以是整型常量,也可以是整型变量。
- 语法规范
- 必须包含头文件
#include <vector>。位于命名空间 std 中(需使用std::vector或 using 声明)。 - 使用尖括号
< >指出存储的数据类型,使用圆括号 ( ) 指定元素个数。
- 必须包含头文件
vector<typeName> vt(n_elem);
- typeName:要存储的数据类型(如 int、double、string 等)。
- vt:对象名称。
- n_elem:可选参数,可以是整型常量或整型变量,用于指定初始元素个数。
#include <iostream>
#include <vector> // 1. 必须包含头文件
int main() {
using namespace std; // 2. vector 位于 std 命名空间中
// 3. 创建一个初始大小为 0 的 int 类型动态数组
vector<int> vi;
// 4. 运行时决定数组大小(动态联编的完美替代)
int n;
cout << "请输入动态数组 vd 的大小: ";
cin >> n;
// 创建一个包含 n 个 double 类型元素的动态数组
vector<double> vd(n);
cout << "成功创建了一个大小为 " << vd.size() << " 的 double 数组。" << endl;
// 自动释放内存:当程序离开 main 函数时,vi 和 vd 的内存会被自动 delete,无需手动干预
return 0;
}
//请输入动态数组 vd 的大小: 10
//成功创建了一个大小为 10 的 double 数组。
指针,数组和指针算术
指针和数组基本等价的原因在于指针算术(pointer arithmetic)和C++内部处理数组的方式。
首先,我们来看一看算术。将整数变量加1后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的字节数。将指向double的指针加1后,如果系统对double使用8个字节存储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2个字节存储,则指针值将增加2。C++将数组名解释为地址。
#include <iostream>
using namespace std;
int main() {
// 1. 定义两个不同类型的静态数组
double wages[3] = {10000.0, 20000.0, 30000.0};
short stacks[3] = {3, 2, 1};
// 2. 获取数组地址的两种等价方式
double *pw = wages; // 方式一:数组名本身就是数组首元素的地址
short *ps = &stacks[0]; // 方式二:使用取地址符 & 指向第一个元素
// ====================================================
// 指针算术:指针加 1 意味着什么?
// ====================================================
// 观察 double 指针操作(每个 double 占 8 字节)
cout << "初始 pw 指针的地址 = " << pw << ", 指向的值 *pw = " << *pw << endl;
pw = pw + 1; // 指针向后移动一个元素的距离
cout << "将 pw 指针加 1 后:\n";
cout << "移动后 pw 的新地址 = " << pw << ", 指向的值 *pw = " << *pw << "\n\n";
// 观察 short 指针操作(每个 short 占 2 字节)
cout << "初始 ps 指针的地址 = " << ps << ", 指向的值 *ps = " << *ps << endl;
ps = ps + 1; // 指针向后移动一个元素的距离
cout << "将 ps 指针加 1 后:\n";
cout << "移动后 ps 的新地址 = " << ps << ", 指向的值 *ps = " << *ps << "\n\n";
// ====================================================
// 两种方式访问数组元素:数组下标法 vs 指针解引用法
// ====================================================
cout << "【方法一】使用数组下标法访问:\n";
cout << "stacks[0] = " << stacks[0] << ", stacks[1] = " << stacks[1] << endl;
cout << "【方法二】使用指针算术和解引用访问:\n";
cout << "*stacks = " << *stacks << ", *(stacks + 1) = " << *(stacks + 1) << "\n\n";
// ====================================================
// 核心区别:sizeof 运算符在数组名和指针上的不同表现
// ====================================================
cout << "sizeof(wages) = " << sizeof(wages) << " 字节 (表示整个数组的总大小:3 * 8字节)\n";
cout << "sizeof(pw) = " << sizeof(pw) << " 字节 (表示指针变量本身的大小,64位系统通常为 8字节)\n";
return 0;
}
/*
初始 pw 指针的地址 = 0x6cd57ff770, 指向的值 *pw = 10000
将 pw 指针加 1 后:
移动后 pw 的新地址 = 0x6cd57ff778, 指向的值 *pw = 20000
初始 ps 指针的地址 = 0x6cd57ff76a, 指向的值 *ps = 3
将 ps 指针加 1 后:
移动后 ps 的新地址 = 0x6cd57ff76c, 指向的值 *ps = 2
【方法一】使用数组下标法访问:
stacks[0] = 3, stacks[1] = 2
【方法二】使用指针算术和解引用访问:
*stacks = 3, *(stacks + 1) = 2
sizeof(wages) = 24 字节 (表示整个数组的总大小:3 * 8字节)
sizeof(pw) = 8 字节 (表示指针变量本身的大小,64位系统通常为 8字节)
*/
- C++ 中的指针加减法是以“指向的数据类型的大小”为单位进行移动的,而不是简单的字节数加 1。
- 对数组名使用 sizeof,得到的是整个数组占用的总内存。
- 对指针使用 sizeof,得到的仅仅是指针变量本身的大小(32位系统是 4,64位系统是 8)
结构体成员
C++新手在指定结构成员时,搞不清楚何时应使用句点运算符,何时应使用箭头运算符。规则非常简单:
- 见名用点「.」:如果变量就是一个实实在在的结构体对象名,直接用点。
- 见针用箭「->」:如果变量是一个指向结构体的指针,必须用箭头。
为什么要有箭头运算符?其实它只是一个语法糖(快捷方式)。当你有一个结构体指针 pStu 时,如果你想用句点运算符,你必须先对指针进行解引用(用 * 号把藏在地址里的实体揪出来),像这样:
(*pStu).name = "李四"; // 必须加括号,因为 . 的优先级比 * 高
但是写 (*pStu).name 实在是太丑了,而且括号很容易漏掉。于是 C++ 创始人一拍大腿,发明了箭头运算符 ->,用来完美替代 (* ).:
pStu->name=(*pStu).name
示例:
#include <iostream>
#include <string>
// 定义一个简单的结构体
struct Student {
std::string name;
int age;
};
int main() {
// ==========================================
// 场景一:结构标识符是【结构名】 -> 使用 句点运算符 (.)
// ==========================================
Student stu1; // 实实在在的对象,在栈上分配
stu1.name = "张三"; // 结构名 . 成员
stu1.age = 18; // 结构名 . 成员
std::cout << "学生姓名: " << stu1.name << ", 年龄: " << stu1.age << "\n\n";
// ==========================================
// 场景二:标识符是【指向结构的指针】 -> 使用 箭头运算符 (->)
// ==========================================
Student* pStu = new Student; // 动态分配,pStu 是一个指针
pStu->name = "李四"; // 指针 -> 成员
pStu->age = 20; // 指针 -> 成员
std::cout << "学生姓名: " << pStu->name << ", 年龄: " << pStu->age << "\n\n";
// 别忘了清理动态分配的内存哦!
delete pStu;
pStu = nullptr;
return 0;
}
//学生姓名: 张三, 年龄: 18
//
//学生姓名: 李四, 年龄: 20
自动存储、静态存储和动态存储
- 自动存储:随借随还,函数内部声明,出局部作用域自动销毁(存放在栈)。
- 静态存储:寿与天齐,程序启动时创建,程序结束时才销毁(存放在静态存储区)。
- 动态存储:生死由你,用 new 申请,必须手动 delete 释放(存放在堆)。
#include <iostream>
// ==========================================
// 1. 静态存储(全局变量):生存期贯穿整个程序,所有函数均可访问
// ==========================================
int globalValue = 100;
void demoFunction() {
// ==========================================
// 2. 自动存储(局部变量):每次进函数创建,出函数立即销毁
// ==========================================
int autoVar = 1;
autoVar++;
// ==========================================
// 3. 静态存储(局部静态变量):出函数不销毁,值会保留供下次使用
// ==========================================
static int staticVar = 1;
staticVar++;
// 修改全局变量
globalValue += 10;
std::cout << "[函数内] 自动变量 autoVar = " << autoVar
<< " | 静态局部变量 staticVar = " << staticVar
<< " | 全局变量 globalValue = " << globalValue << std::endl;
}
int main() {
std::cout << "--- 第一次调用函数 ---" << std::endl;
demoFunction();
std::cout << "\n--- 第二次调用函数 ---" << std::endl;
demoFunction(); // 观察 staticVar 和 globalValue 的累加效应
std::cout << "\n--- 主函数读取全局变量 ---" << std::endl;
std::cout << "[main] 全局变量最后的值 = " << globalValue << std::endl;
// ==========================================
// 4. 动态存储(堆内存):生命周期完全由程序员手动控制
// ==========================================
std::cout << "\n--- 动态存储演示 ---" << std::endl;
int *dynamicVar = new int(500); // 申请堆内存
std::cout << "[main] 动态变量的值 = " << *dynamicVar << std::endl;
delete dynamicVar; // 手动释放内存,归还系统
dynamicVar = nullptr; // 安全置空,防止野指针
return 0;
}
/*
--- 第一次调用函数 ---
[函数内] 自动变量 autoVar = 2 | 静态局部变量 staticVar = 2 | 全局变量 globalValue = 110
--- 第二次调用函数 ---
[函数内] 自动变量 autoVar = 2 | 静态局部变量 staticVar = 3 | 全局变量 globalValue = 120
--- 主函数读取全局变量 ---
[main] 全局变量最后的值 = 120
--- 动态存储演示 ---
[main] 动态变量的值 = 500
*/
指针避坑
在 C++ 的世界里,指针就像是一把“双刃剑”:它赋予了你直接操控底层内存的至高权力,让程序拥有极致的性能;但同时,它没有任何安全防护网,任何一次粗心大意(比如操作野指针、重复释放)都会让计算机直接崩溃。
- 未初始化不使用:声明指针时,如果不立即指向有效地址,必须初始化为 nullptr。绝对不能让它变成随机指向的“野指针”。
- 配对原则不妥协:代码里写了几个 new(或 new[]),就必须严格对应几个 delete(或 delete[])。不多不少,刚好配对。
- 释放内存后立位置空:一旦 delete 完一个指针,下一行代码必须将其赋值为 nullptr。这是防止“二次释放”最有效的物理防线。
#include <iostream>
int main() {
// ==========================================
// :red_circle: 危险行为 1:使用未经初始化的指针(直接导致程序崩溃)
// ==========================================
// int* pBad;
// *pBad = 100; // :cross_mark: 绝对禁止!此时 pBad 指向一个随机的未知内存地址
// :green_circle: 正确做法:暂时不用就初始化为 nullptr
int* pGood = nullptr;
// ==========================================
// :red_circle: 危险行为 2:试图释放同一个内存块两次(Double Free)
// ==========================================
int* pMemory = new int(42);
delete pMemory;
// delete pMemory; // :cross_mark: 严重错误!二次释放会导致运行时崩溃
// :green_circle: 正确做法:释放后立即置空,后续再 delete 它就是绝对安全的
int* pSafe = new int(88);
delete pSafe;
pSafe = nullptr; // 筑起防线
delete pSafe; // :check_mark_button: 安全:对 nullptr 进行 delete 编译器会自动忽略
return 0;
}
模板类 array
模板类 array 是一种固定长度的静态数组 。它是传统数组的现代化、安全替代品。它与传统数组一样,将数据存储在栈(静态内存分配)而非堆(自由存储区)中,因此运行效率与传统数组完全相同,但提供了更高的便捷性和安全性。
- 长度固定:在编译时就必须确定大小,一旦创建,长度不可改变。
- 高效安全:拥有传统数组的高执行效率,同时兼具容器的便利性(如支持迭代器、边界检查等)。
- 大小必须为常量:指定元素个数的参数(n_elem)只能是整型常量(或常量表达式),绝对不能是变量。
- 语法规范
- 必须包含头文件
#include <array>。 - 位于命名空间 std 中。
- 声明时,尖括号
< >内需要同时指出数据类型和元素个数。
- 必须包含头文件
array<typeName, n_elem> arr;
demo:
#include <iostream>
#include <array> // 1. 必须包含头文件
int main() {
using namespace std;
// 2. 创建一个包含 5 个 int 元素的 array 对象(未初始化,内部是随机值)
array<int, 5> ai;
// 3. 创建并列表初始化一个包含 4 个 double 元素的 array 对象
// (修复了原书文字大括号被识别为圆括号的错误)
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};
// 4. 错误示例演示(解除注释会导致编译报错):
// int n = 5;
// array<int, n> error_arr; // :cross_mark: 错误:n 是变量,n_elem 不能是变量!
// 5. 使用 const 常量则是允许的:
const int size = 3;
array<string, size> arr_str = {"C++", "Java", "Python"};
cout << "ad 数组的第一个元素: " << ad[0] << endl;
cout << "arr_str 数组的大小: " << arr_str.size() << endl;
// 内存自动管理:由于数据在栈上,函数结束时内存会自动释放
return 0;
}
//ad 数组的第一个元素: 1.2
//arr_str 数组的大小: 3