C++ Primer Plus(第6版)读书笔记【1】

前言

本书介绍的是通用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

正文完
 0
评论(没有评论)