# C++ 期末复习 2024

* 本科生学业指导中心
  * 逸夫楼 201
* 王尧勇
  * 哔哩哔哩 [王尧勇](https://space.bilibili.com/226865405)
  * 计算机科学与工程学院 2020级 计算机科学与技术专业

## 0. 前言

* 编程语言是人类定义的规则，如果制定 C++ 规则的人的思维是正常的，那么 Ta 的这些规则大抵应该是符合逻辑的。
* 于是我们可以从规则制定者的角度去审视编程语言的语法，即设计这个东西出来是用来干什么的？
  1. 这个语法可以做到之前的语法做不到的事情(比如结构控制语句)。
  2. 这个语法可以更加方便地做到之前语法做起来很麻烦的事情(比如函数)。
  3. 这个语法是为了弥补之前某个语法造成的问题。

## 1. 基本类型和变量

### 1.0 关键字和标识符

* 关键字就是语言本身要用到的一些名字，本身表示了某种功能，因此不能再用它们来命名。
* 标识符，就是某个东西的名字，最常见的就是变量的命名，遵循以下规则：
  * 字母、数字、下划线 组成
  * 数字不能作为第一个字符
  * 不能和关键字一样
* 注意，关键字严格按照课本所列举的记忆，`cin`、`cout` 不是关键字，`define` 、`include` 不是关键字，等等。

### 1.1 基本类型

5种基本类型：布尔型(`bool`)、字符型(`char`等)、整型(`int`等)、浮点型(`double`等)、空类型(`void`)。

<figure><img src="/files/nr0MNmBmmX7rzXuS43Wm" alt=""><figcaption></figcaption></figure>

类型，就是对内存中的 `01` 串的不同角度的诠释。

**布尔型**

* 布尔型直接 `cout` 会输出数字，如果要输出 `true` 或 `false`，则 `cout << boolalpha <<`

```cpp
#include <iostream>
using namespace std;

bool flag = true;
cout << flag << "\t" << !flag << "\n";
cout << boolalpha << flag << "\t" << !flag << "\n";
```

* 布尔型转整型：`true` 为 `1`，`false` 为 `0`
* 整型转布尔型：`0` 为 `false`，非 `0` 为 `true`

**整型**

* 整数罢了，但是存的都是补码的形式

<figure><img src="/files/tiGmdKlzeHy2BsZ8uExQ" alt=""><figcaption></figcaption></figure>

* 取反加一？都什么年代，还在算传统补码😋

$$
X\_{补码} = 2^n - |X|
$$

例如，对于 `8` 位的整型，`-3` 在内存中是以补码存储，也就是 `256 - | -3 | = 253 = 1111 1101`

**浮点型**

* 浮点数存储原理和整数很不一样(IEEE 754)
* 存在误差，可能十进制下的有限小数在二进制下是无限循环的，因此可能会有精度的损失
* 因此判断一个浮点数是否等于某个数不能直接用 == 判断

```cpp
double x = 2.0 + 1.14;

if(x == 3.14) {
	cout << "nya\n";
}
```

```cpp
#include <cmath>

double x = 2.0 + 1.14;
if(fabs(x - 3.14) < 1e-5) {
	cout << "nya\n";
}
```

**字符型**

* 字符型在内存中就是按照整数存储的
* 整数和字符的映射规则就是 ASCII 码

```cpp
cout << "A : " << int('A') << endl; 
cout << "a : " << int('a') << endl; 
cout << "0 : " << int('0') << endl; 
```

```cpp
char str[] = "114514";
int num = 0;
for(int i = 0; str[i] != '\0'; i++) {
	num = num * 10 + (str[i] - '0');
}
cout << num << endl;
```

* 课本中和考试中，`char` 都是默认有符号的，因此，如果你见到一些故意找茬的赋值语句：

```cpp
char a = 0;

char b = -128;

unsigned char c = -159;

unsigned char d = -1;

cout << (int)a << endl << (int)b << endl << (int)c << endl << (int)d << endl;
```

### 1.2 字面值

* **字面值的含义就是其字面意义：字面上的值**😅
* 整型值的不同进制表示：无前缀默认为10进制、数字0开头表示8进制、0x或者0X开头表示16进制、0b或0B开头表示2进制。

```cpp
int a = 038;
```

* 浮点值的定点表示法：不加后缀默认为`double`、加后缀f或F为`float`、加后缀l或者L为`long double`
* 浮点值的科学表示法：`尾数e指数` 或 `尾数E指数`，注意 e 后面只能是整数
* 转义字符
  * 转义字符的主要目的是为了表示键盘上不方便敲进代码的键位，例如`‘\n’`、`‘\t’`等
  * 另外转义字符也可以用ASCII码来表示字符，但只能用8进制或16进制，8进制0开头例如`'\023'`、16进制x开头例如`'\xa'`

```cpp
#include <cstring>

const char *str = "abcd\'efg\028";

cout << strlen(str) << endl;

cout << sizeof(str) << endl;
```

* 转义字符的功能超乎你想象

[ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code)

```cpp
cout << "\033[30m 黑色\n";
cout << "\033[31m 红色\n";
cout << "\033[32m 绿色\n";
cout << "\033[33m 黄色\n";
cout << "\033[34m 蓝色\n";
cout << "\033[35m 紫色\n";
cout << "\033[36m 青色\n";
cout << "\033[37m 白色\n";
```

### 1.3 变量

* 变量初始化

```cpp
int a = 114;	// 传统初始化, 不做安全检查
int b(514);		// 对象式初始化, 不做安全检查
int c{19};		// C++11新标准, 有安全检查
```

```cpp
unsigned int x {-3};
```

* `auto` 自动推导

```cpp
auto a = 3.14;
auto b = 3.14f;
auto c = 3.14l;
auto d = 3;
auto e = false;
auto f = 'x';

cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
```

```cpp
auto p1 = new double;
auto *p2 = new double(2.33);
auto p3 = new double[3]{0, 1, 2};
auto p4 = *new double[3]{0, 1, 2};

cout << typeid(p1).name() << endl;
cout << typeid(p2).name() << endl;
cout << typeid(p3).name() << endl;
cout << typeid(p4).name() << endl;
```

* `const` 命名常量

```cpp
int const a = 114;
const int b = 514;
```

## 2. 运算符和表达式

### 2.0 杂项

* 表达式没有分号，有分号的是语句捏
* 运算符优先级
  * 考前背就完事了，真记不住所有的
  * **优先结合并非优先计算**

```cpp
int x = 2 + 3 + 4 * 5;
```

* 表达式的返回值

```cpp
int a, b;
a = b = 3;
```

### 2.0 算数运算符

* 单目运算符：`-` 负数运算符，`+` 正数运算符；
* `+` 加、`-` 减、`*` 乘；
* `/` 除：除数不能为 0，整型和整型运算结果为整型；浮点数运算，结果为浮点数；

```cpp
double r;
cin >> r;
double s = 1/2*r*r;
cout << s;
```

* `a%b` 取模：b 不能为 0，结果的符号和 a 一致，运算变量不能是浮点数

```cpp
cout << 5  %  3 << "\n";
cout << -5 %  3 << "\n";
cout << 5  % -3 << "\n";
```

* 开平方使用 `sqrt`函数(`cmath` 或 `math.h` 头文件)，返回值类型为 double；
* 没有数学上的 $a^b$ 运算符，可以使用 `cmath` 中的 `pow` 函数，不要用 `^`，这是异或捏😫

### 2.1 关系运算符

* 6个关系运算符：`<`、`<=`、`>`、`>=`、`==`、`!=`;
* 结果为布尔类型
* 当两个参与比较的左右操作数类型不同时，自动转换为更高精度的类型再比较

```cpp
int c = -4;
unsigned a = 2;
cout << boolalpha << (c<a) << endl;
```

* `==` 不要写成 `=` 😅

```cpp
int x;
cin >> x;
while(x = 3) {
	cout << "Ciao\n";
	x++;
}
```

### 2.2 逻辑运算符

* 3个逻辑运算符：`!` 逻辑非、`&&` 逻辑与、`||` 逻辑或；
* `!`的优先级高于算数运算符，`&&`和`||`的优先级低于算术运算符和关系运算符；
* `&&`和`||`的短路特性

```cpp
int a = 2, b = 3;
int c = a > b && b++;
int d = a < b || a++;
cout << a << " " << b << " " << c << " " << d << endl;
```

* 不考虑优先级?🧐

```cpp
int a = 3, b = 2;
b == 2 || a++;
```

### 2.3 条件比较运算符

* 条件运算符

```cpp
<表达式1> ? <表达式2> : <表达式3>
```

<表达式1>为真则求解<表达式2>，其值作为整个式子的返回值，<表达式3>不会执行

<表达式1>为假则求解<表达式3>，其值作为整个式子的返回值，<表达式2>不会执行

```cpp
int a = 2, b = 3;
int c = a > b ? a++: b++;
cout << a << " " << b << " " << c << endl;
```

* 比较前，当<表达式2>和<表达式3>类型不同时，自动转换为更高精度的类型

```cpp
unsigned int a = 2;
int b = -1;
auto c = false ? a : b;
cout << c;
```

### 2.4 位运算

* `<<` 左移：右边补0，带符号数溢出可能会改变符号

```cpp
int a = -34359738367;
int b = a << 1;
cout << b;
```

* `>>` 右移：带符号数左边补符号数，无符号数左边补0
* 若移动位数为负数，VS和DEV C++会警告，最后结果为0
* 移动位数不要超过变量的位数，否则按取模算
* `~` 取反、`&` 按位与、`|` 按位或、`^` 按位异或
* 优先级：取反 > 按位与 > 按位异或 > 按位或
* 操作数必须是整型
* 位运算均针对补码

```cpp
int a = -3 ^ 5;
cout << a;
```

### 2.5 赋值运算符

* 优先级很低(仅高于逗号运算符)
* 结合性自右向左：`a=b=c;`相当于`a=(b=c);`
* 赋值运算符左边必须是左值，赋值运算符的返回值也是左值，即可以`(a=5)=6;`

```cpp
int a = 10, b;
a = b = 1;
```

```cpp
(b = a) = 1;
```

```cpp
++a = 1;
```

```cpp
(a++) = 1;
```

```cpp
a = b/2 = 3;
```

### 2.6 逗号运算符

* **优先级最低**🤣🤣🤣
* 从左到右计算，最右边的表达式作为整个式子的值

```cpp
auto x = (3, 4+7, 3*5, 233);
cout << x;
```

```cpp
int a = (cout << "1 ! ", cout << "5 !", 15);
```

* 若最右边是一个左值，逗号表达式的值也是左值

```cpp
(42, x) = 42;
```

* 坑😡

```cpp
int a = 2, b = 3, c;
c = a+b, a-b, a*b;
cout << c;
```

### 2.7 自增自减运算符

以自增运算为例。

* 前置 i++
  1. +1
  2. 返回 变量i 的值
* 后置 ++
  1. `j = i`
  2. `i = i+1`
  3. 返回 `j`
* ++的优先级高于所有算术运算符，且后置高于前置
* ++必须是左值，前置++返回左值，后置++返回右值
* 自增和加法一起用容易混淆，甚至引发错误，不要这么干

```cpp
c = a+++++b;
```

* 同时自增运算很容易出现**未定义行为**

```cpp
int a = 3;
int b = ++a + ++a;
cout << b;
```

[链接](https://godbolt.org/z/z38bze8G1)

### 2.8 sizeof

* **`sizeof` 是一个运算符，不是一个函数！**
* `sizeof(类型名)`或`sizeof(表达式)`，前者编译时计算完毕，后者运行时计算；
* `sizeof(指针)`得到的是指针变量的大小，例如32位系统大小为4,64位系统大小为8；
* `sizeof(数组名)`得到的整个数组所占字节数，再除以`sizeof(数组元素类型)`可得数组元素个数；

### 2.9 类型转换

<figure><img src="/files/ikc3OhdAXsv0Q9ViShAW" alt=""><figcaption></figcaption></figure>

* 整型提升
* 强制类型转换

```cpp
y = double(a)/b;
y = (double)a/b;
```

* 类型转换可以深究很多，好在考试不会这么难

### 2.10 `typeid`、 `decltype`

## 3. 基本语句

### 3.0 `if`

### 3.1 `switch`

* 如果正常人写出来的 `switch` 语句，不会有什么难度，然而考试考的都是不正常的😅
  * `case`中无`break`
  * `default` 不在最下面
* `case` 和 `default` 只决定从哪里开始执行，一旦确定之后，便是从上往下执行，直到执行完或者遇到 `break`

```cpp
int a = 0;
while(a < 5) {
    switch(a) {
        case 0:
            cout << "地振高冈\n";
            a++;
        case 1:
            cout << "一派溪山千古秀\n";
            a += 2; 
        default:
            cout << "门朝大海\n";
        case 2:
            cout << "三河合水万年流\n";
            break;
        case 3:
            cout << "天父地母\n";
            a++;
        case 4:
            cout << "反清复明\n";
            a++;
    }
}
```

### 3.2 `while`

### 3.3 `for`

* 什么B题目啊😅

```cpp
int i = 0;
cout << "hello";
    
for(i = 0; i <= 5; i++);
{
    if(i != 6)
        cout << i;
}
```

### 3.4 跳转语句

* `break`
* `continue`
* `goto`

```cpp
#include <iostream>
using namespace std;
    
int main() {
    http://www.baidu.com
    return;
}
```

## 4. 函数

### 4.0 函数和主函数

* `main` 函数的参数

```cpp
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
    cout << argc << endl;
    for(int i = 0 ; i < argc; i++) {
        cout << argv[i] << endl;
    }
    return 0;
}
```

### 4.1 值传递、引用传递、指针传递

* 值传递

```cpp
#include <iostream>
using namespace std;

void mySwap(int a, int b) {
	int t = a; a = b; b = t;
}
int main() {
    int a = 2, b= 3;
    mySwap(a, b);
    cout << a << " " << b;
    return 0;
}
```

* 为什么没有交换，怎么会事捏？🤨
* 引用传递

```cpp
#include <iostream>
using namespace std;

void mySwap(int &a, int &b) {
	int t = a; a = b; b = t;
}
int main() {
    int a = 2, b= 3;
    mySwap(a, b);
    cout << a << " " << b;
    return 0;
}
```

* 指针传递（地址传递）

```cpp
#include <iostream>
using namespace std;

void mySwap(int *p, int *q) {
	int t = *p; *p = *q; *q = t;
}

int main() {
    int a = 2, b= 3;
    mySwap(&a, &b);
    cout << a << " " << b << endl;
    return 0;
}
```

* 指针传递的坑

```cpp
void f(int *s) {
    s = new int;
    *s = 10;
}

int main() {
    int a = 5;
    int *s = &a;
    f(s);
    cout << a << endl;
    return 0;
}
```

```cpp
void swap(int *p, int *q) {
    int *t = p;
    p = q;
    q = t;
}

int main() {
    int a = 2, b= 3;
    int *p = &a, *q = &b;
    swap(p, q);
    cout << *p << " " << *q << endl;
    return 0;
}
```

```cpp
void swap(int* &p, int* &q) {
    int *t = p;
    p = q;
    q = t;
}
```

### 4.2 函数重载

* 讨论函数重载时，把自己想象为编译器，`可爱的小编译器一枚吖`😘
* 形参个数和类型不同就可以重载
* 核心在于让编译器知道是两个不同的函数，在传入参数的时候不会混淆

```cpp
int f(int a);
int f(int a, int b);
int f(double a);

f(3)
f(2, 3)
f(3.14)
```

* 返回值类型不同不能用来重载
* 有默认值的情况可能会有干扰：

```cpp
int f(int a);
int f(int a, int b = 3);

f(3)
```

* `const`修饰不能作为重载的依据（仅限这种最简单的情况下，涉及到指针和引用的const就复杂了）

```cpp
int f(int a);
int f(const int a);
```

* 重载函数调用时的最佳匹配：
  1. 严格匹配
  2. 整型提升
  3. 赋值转换
  4. 有多个形参时情况复杂，核心还是没有二义性；

```cpp
void f(double a) {
    cout << a*a;
}
    
void f(int a) {
    cout << a + a;
}
    
void f(char a) {
    cout << a;
}

f('a')
```

* 函数重载的类型匹配很复杂，好在考试不会涉及到更复杂的（应该吧）

### 4.3 默认参数

```cpp
int f(int a = 1, int b = 2, int c = 3);
```

```cpp
int f(int a = 1, int b, int c);
```

```cpp
int f(int a = 1, int b, int c = 3);
```

```cpp
int f(int a, int b = 2, int c);
```

```cpp
int f(int a = 1, int b = 2, int c);
```

* 必须优先保证右边的参数有默认值，不能出现左边有，而右边没有的情况

### 4.4 变量作用域

* 每当使用变量时，使用的是最近的还存活着的那个变量

```cpp
#include <iostream>
using namespace std;

int a = 233;

void f() {
    int a = 666;
    cout << a << endl;
}

int main() {
    int a = 3;
    cout << a << endl;
    {
        int a = 5;
        cout << a << endl;
    }
    f();
    
    cout << ::a << endl;
    return 0;
}
```

### 4.5 变量类型

* 平平无奇的非静态局部变量

```cpp
void f() {
    int a = 3;
    cout << a << endl;
    ++a;
}
    
int main() {
    f();
    f();
    f();
    return 0;
}
```

* 静态局部变量：生命周期是全程的，但只能在函数内使用
* 孟婆汤掺水了

```cpp
void f() {
    static int a = 1;
    a++;
    cout << a;
}

int main() {
    f();
    f();
    f();
    return 0;
}
```

* 静态全局变量：单个文件内部共享
* 非静态全局变量：多文件共享
* `extern`修饰

```cpp
int a;  // a有初始值0
extern int b = 3; // 必须显式初始化
```

### 4.6 函数的递归

* 函数可以调用函数，函数可以调用自己
* 盗梦空间是吧

```cpp
#include <iostream>
using namespace std;

int f(int n) {
    if(n == 1)
        return 1;
    if(n == 2)
        return 1;
    return f(n-1) + f(n-2);
}
    
int main() {
    cout << f(5);
    return 0;
}
```

```cpp
#include <iostream>
using namespace std;

int f(int a[], int s, int e) {
    if(s == e)
        return a[s];
    return a[s] + f(a, s+1, e);
}

int main() {
    int a[] = {1, 2, 3, 4, 5, 6, 7};
    cout << f(a, 2, 5);
    return 0;
}
```

### 4.7 可变参数

```cpp
int f(int a, ...) {
    
}
```

### 4.8 `constexpr` 函数

* 编译时期确定值
* 只要在编译时、运行前能知道函数的结果，那就不会报错

```cpp
constexpr int f(int n) {
	if(n == 0)
		return 0;
	if(n == 1)
		return 1;
	return f(n-1) + f(n-2);
}

f(3);

const int m = 4;
f(m);

constexpr int m = 4;
f(m);

int n;
cin >> n;
f(n);
```

## 5. 编译预处理

* 宏命令末尾没有分号

### 5.0 文件包含

* `#include <filename>` 或者 `#include "filename"`
* 纯纯的复制的效果

```cpp
#include <iostream>
using namespace std;
    
int main() {
    int a[] = {
        #include "data.txt"
    };
    
    for(int i = 0; i < 5; ++i) {
        cout << a[i] << " ";
    }
    return 0;
}
```

### 5.1 无参宏

* 作用域是定义的那一行以下直到文件末尾
* 宏定义和变量重名

```cpp
#include <iostream>
using namespace std;
    
int main() {
    double PI = 3.14;
    cout << PI << endl;
    #define PI 3
    cout << PI << endl;
    return 0;
}
```

* 子字符串不会被替换

```cpp
#define 华为 苹果

cout << "刘德华为什么不演坏人";

#define av xx

cout << "java";
```

### 5.2 有参宏

* 十分机械的字符串替换

```cpp
#include <iostream>

#define F(a) a * a

int main() {
    int a = 2, b = 3, c = 4;
    int d = F(b - a)*c; 
    cout << d;
    return 0;
}
```

## 6. 结构体、枚举、联合体

### 6.0 结构体

* **在 C++ 中**，结构体和类唯一的区别就是默认的访问修饰符不一样，结构体默认 `public`，类默认 `private`，除了这一点，其他的特性完全一样。
* 常见考题

```cpp
struct s {
    int n;
    int *m;
};
    
int d[3] = {10, 20, 30};
s arr[3] = {100, &d[0], 200, &d[1], 300, &d[2]}, *p = arr;
    
cout << ++(p->n) << " ";
cout << (++p)->n << " ";
cout << *++(p->m);
```

### 6.1 结构体的字节对齐

### 6.2 结构体的嵌套定义

```cpp
struct A {
    int a;
    struct B{
        int b;
    };
    B c;
};
```

```cpp
struct A {
    int a;
    struct B{
        int b;
    } c;
};
```

* 不能嵌套定义自己

```cpp
struct A {
    int n;
    A b;
};
```

* 但是嵌套自己的指针可以（链表的节点）

```cpp
struct A {
    int n;
    A *next;
};
```

### 6.3 位域

### 6.4 枚举

* 枚举类型的定义

```cpp
enum Weekday {Mon, Tue=3, Wed, Thur, Fri, Sat,Sun};
```

不指定值，其值就是前一个+1

指定值，就是指定的值

第一个如果不指定值，那就是 0

* 枚举变量的定义

```cpp
enum Weekday {Mon, Tue, Wed, Thur, Fri, Sat,Sun};
    
Weekday week = Mon;
    
int i = Mon;
i = week;
week = i; 
week = (Weekday)i;
```

* 枚举 和 `int` 的转换
  * 枚举常数会隐式转换成 `int`，可以把枚举值赋值给 `int` 变量
  * 但是反过来不会，所以不能把一个 `int` 值赋值给枚举变量
  * 枚举值参与算术运算时隐式转换为 `int` 参与运算，结果也不再是枚举类型
* 典中典的一道题

```cpp
enum e1{e2, e3, e4, e5} e6(e5);
    
int main() {
    e6 = e3;
    auto e6 = e3 | e4;
    cout << (e6 == e5) << endl;
}
```

### 6.5 强类型枚举

### 6.6 联合体

* 百变怪罢了

### 6.7 `typedef` 和 `using`

```cpp
using w = char[10];

w a[20];
```

## 7. 指针

### 7.0 一些说明

* 学习指针，至少从备考的角度来说，要更多的关注 **怎么用**，而非更多关注 **是什么**
* 说明几个概念，下文中都是表示如下意思：
  * 指针：指的是各种指针**变量**
  * 地址：指的是 `0x12356ff` 这样的值，指针里面存的就是地址
* 学习一个指针（或者一个类似指针的东西，比如数组名），我们只要关心其以下几个特点：
  1. 级别
  2. 指向类型
  3. **自身指向的可变性**
  4. **指向的内容的可变性**

### 7.1 内存和地址

* 指针相关的题，画好图就可以帮助解题

### 7.2 指针和多级指针

* 为什么需要指针？
  * ~~为了考试难~~
  * 灵活，太灵活了
    * 动态开辟内存
    * 控制数组、控制函数、控制变量
    * 跨作用域
* 指针为什么分这么多不同类型？
  * 在语法层面进行区分，不然都是 `void*` 不好读
  * 指针加减法时的实际地址变化不一样

```cpp
const char * s = "HelloWorld";

cout << (s + 3) << "\n";

int * p = (int*)s;
p++;

cout << (char*)p << "\n";
```

* 指针的定义

```cpp
int a = 2, b = 3;

int *p = &a;
    
int* q = &b;
    
int c, *r = &c;
```

* 多级指针
  * 套娃思想
  * 指针其实就是个用来存地址的变量，那么指针变量本身也有对应的内存地址
  * 变量的地址可以被指针变量存储，即指针可以指向一个普通变量，那么也可以有一种变量用来存储指针变量的地址，即二级指针

```cpp
int a, b;
int* p = &a;
int* q = &b;
int** pp = &p;
        
**p = 233;
*PP = &b;
**p = 666;
```

### 7.3 数组

* 数组定义

```cpp
int a[13];
```

* 数组大小只能为正整数常量（包括const常量、C++11中`constexpr`函数），实际使用时发现编译器也允许用变量定义数组大小，但这不好🥵
* 数组初始化

```cpp
int a[5] = {1, 2, 3, 4, 5};
int a[5]{1, 2, 3, 4, 5};
int a[5] = {1, 2, 3};
int a[] = {2, 3, 3, 3};
```

* 对部分元素初始化，后面没初始化的默认为0
* 对所有元素赋值，可不指定长度，根据所给个数确定
* 定义时没初始化，之后就不能用大括号初始化了

```cpp
int a[5];
a[5] = {1, 2, 3, 4, 5};
```

* 访问数组时注意下标为`0~n-1`，下标越界语法上没有错误，会访问到未知的内存

```cpp
int a[3] = {0, 1, 2};
cout << a[-1];
```

* 数组和数组不能整体赋值

```cpp
int a[3] = {1, 2, 3}, b[3];
b = a;
```

* 不要直接比较数组名来判断两个数组是否相等（实际上在比较地址）

```cpp
int a[3] = {1, 2, 3}, b[3] = {1, 2, 3};
if(b == a) {
	
}
```

以下方式对str的声明中，不能将str作为字符串使用的是：

```cpp
char str[9] = {'N', 'J', 'U', 'S', 'T'};
```

```cpp
char str[] = {"NJUST"};
```

```cpp
char str[] = "NJUST";
```

```cpp
char str[] = {'N', 'J', 'U', 'S', 'T'};
```

* 二维数组
  * 二维数组可以看成一维数组的数组
  * 二维数组的初始化：核心：**列宽**

```cpp
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
```

```cpp
int a[][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
```

```cpp
int a[][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
```

```cpp
int a[][3] = {{1, 3, 5}, {2, 4, 6}};
```

```cpp
int a[3][4] = {{1}, {5, 6}, {9, 10, 11}};
```

* 实际存储是一维的

### 7.4 指针和数组

* 数组在很多时候，表现得像一个指针，但是它不是指针，不是指针
* 之所以这样，是很多时候我们使用数组的时候，它很容易就隐式转换成指针类型了

<figure><img src="/files/QIili9uF7ONu73D7DyFf" alt=""><figcaption></figcaption></figure>

***注：本人仅使用这张图代表的梗来和数组与指针类比，达到一种幽默诙谐的教学作用，并不代表本人认同或者不认同原图代表的观点和言论。***

* 数组是一个常量，**值不能被改变**，**值不能被改变**，**值不能被改变**

```cpp
int a[3] = {1, 2, 3};
a++;
++a;
```

```cpp
char s[] = "Hello";
s = "world";
```

```cpp
int a[3] = {1, 2, 3};
int* p = a;
p++;
char s[] = "Hello";
char* q = s;
++q;
```

* 一级指针可以指向一个数组，就是指向数组首元素，把一个数组赋值给指针，是隐式转换为了常量指针赋值给指针

```cpp
int a[3] = {1, 2, 3};
int* p = a;
int* p = &a[0];
```

* 下标访问 和 解引用 等价

```cpp
int a[3] = {1, 2, 3};
int* p = a;
        
*(p + 2) = 114;
p[2] = 114;
        
*(a + 2) = 114;
a[2] = 514;
        
cout << 2[a];
```

* 当然了，还是有点差别，这俩优先级不一样，`[]` 优先级高于 `*`
* 解引用和自增自减运算

```cpp
int a[5] = {1, 2, 3, 4, 5}, *p = a;
cout << *(p++);
cout << *p++;
cout << *++p;
cout << ++*p;
```

若有`int a[]{1, 3, 5, 7, 9}, *p = a+2;`，则执行表达式`int b = 1 + (*p++);`后，`b`和`*p`的值为多少？

A. 6 6\
B. 6 7\
C. 7 6\
D. 8 7

* 二维数组也同样表现得像一个二级指针常量，其指向不能被改变

```cpp
int a[2][3], b[2][3];
a++;
b = a;
a[0]++;
a[0] = b[0];
```

* 上面提到可以把数组视作指针常量来做题，但在以下情形中，二者有区别
  * `sizeof`运算符
  * `&` 取地址符

```cpp
int a[3] = {1, 2, 3};
int (*p)[3] = &a;
```

### 7.5 指针数组和数组指针

#### 7.5.0 指针数组

* 数组的元素是丰富多彩的，可以是基本类型。可以是结构体，可以是对象，那当然也可以是指针
* **指针数组** 就是一个普通的数组，只不过它的每个元素都是指针类型
* 而数组本身又表现得像一个常量指针，故它可以看作和二级指针同级别

```cpp
int* p[3];

int a, b, c;

p[0] = &a;
p[1] = &b;
p[2] = &c;
```

#### 7.5.1 数组指针

* 是一个指针
* 指向数组的指针
* 用于指向一个特定列宽的二维数组，其级别和二维数组相似

```cpp
int a[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int (*p)[3] = a;
int (*p)[3] = &a[0];
```

<figure><img src="/files/5jKBtFopMQxKI3ic79VY" alt=""><figcaption></figcaption></figure>

* 在数组指针指向p之后，即可用p访问二维数组a

```cpp
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int (*p)[4] = a;
*(*(p + 1) + 2) = 114;
p[1][2] = 114;
```

```cpp
int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
int (*p)[4] = a;
int *q[3] = {a[0], a[1], a[2]};
```

若有

```cpp
int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (*p)[4] = a;
```

则`++*(*++p)`的值为多少？

### 7.6 二级指针总结

|                | 自身的可变性 | 指向的内存的可变性 |
| :------------: | :----: | :-------: |
|   `int** p;`   |    Y   |     Y     |
| `int a[2][3];` |    N   |     N     |
| `int (*p)[4];` |    Y   |     N     |
|  `int *b[4];`  |    N   |     Y     |

### 7.7 字符数组和字符串

* C++基本类型中没有字符串，用**字符数组**或**字符指针**的形式来表示字符串(当然还有string)

```cpp
char s[10];
char str[4][20];
```

* 第一行是定义了一个字符数组，可以存字符串，最大长度为10（包括了最后的`\0`）
* 第二行是定义了一个二维数组，也可以看做一个字符串的一维数组，可以存4个字符串，每个字符串最大长度是20（包括了最后的`\0`）
* 字符数组的初始化

```cpp
char s[5] = {'N', 'J', 'U', 'S', 'T'};
char s[10] = {'N', 'J', 'U', 'S', 'T'};
char s[2][10] = {{'N', 'J', 'U', 'S', 'T'}, {'C', 'h', 'i', 'n', 'a'}};
    
char s[10] = "NJUST";
char s[] = "NJUST";
char s[10] = {"NJUST"};
char s[][10] = {"NJUST", ""};
```

### 7.8 字符指针

* 字符指针本身只有一个指针变量的大小，可以指向一个常量字符串

```cpp
char *str = "NJUST";
```

```cpp
const char *str = "NJUST";
```

* 或者指向一个已有的字符数组

```cpp
char s[10] = "NJUST";
char *str = s;
```

### 7.9 字符数组和字符指针

```cpp
char a[] = "hello";
```

```cpp
char *p = "hello";
```

<figure><img src="/files/SEG21yms0djMQoxps9to" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/cbNKn1NEriGkJTltw7kp" alt=""><figcaption></figcaption></figure>

* 字符二维数组和字符指针数组

```cpp
char s1[3][8] = {"I", "love", "NJUST"};
```

```cpp
char *s2[3] = {"I", "love", "NJUST"};
```

<figure><img src="/files/e6V8qMWOQf25Pq7JWsOZ" alt=""><figcaption></figcaption></figure>

若有`char *s1 = "C++";`和`char s2[] = "C++";`，则下面选项不会导致错误的是：

```cpp
s2 = "Java";
```

```cpp
strcpy(s2, "java");
```

```cpp
s1 = "java";
```

```cpp
strcpy(s1, "java");
```

### 7.10 字符串的输入输出

* `cin`或者`cin.get`，利用循环，一个字符一个字符读入，其中`cin`会跳过空格一类的字符，`cin.get`可以读取空格、制表符、换行符；
* `cin>>str;`直接将一个个字符读入`str`所在内存，末尾自动加`\0`，`cin`会被空格、制表符、换行符截断；
* `cin.getline(str, 10);`读入字符串，可读入空格、制表符，遇到回车结束；
* 只有指向变量的指针或者数组名才可以输入；
* `cout << 字符指针或字符数组`，从给定位置一直输出字符直到遇到`\0`；

### 7.11 字符串处理函数

* 字符串处理函数在头文件`<cstring>`（`<string.h>`）中；
* `strlen(str)`，返回`int`类型的字符串长度（不包括\0），`str`可以使字符数组，也可以是字符指针，也可以是字符串常量；
* `strcpy(s1, s2)`，返回值指向第一个参数所指位置的字符指针，从位置`s1`开始，把`s2`开始的字符串一个一个拷贝过来，`s1`必须是指向可修改的变量内存的，注意数组越界问题

```cpp
char s1[] = "Peppa", s2[] = "Pig";
strcpy(s1, s2);
strcpy(s1 + 2, s2 + 1);
```

* `strcat(s1, s2)`，返回指向第一个参数所指位置的字符指针，将字符串`s2`拼接到`s1`后面，`s1`必须是指向可修改的变量内存的，注意数组越界问题；
* `strcmp(s1, s2)`，返回一个`int`整数，从前往后逐个位置比较`s1`和`s2`中对应位置的字符的ASCII码的值，若`s1`大于`s2`，则返回值大于0，若`s1`小于`s2`，则返回值小于0，相等则返回值为0；
* VS中提供了`strcpy_s`、`strcat_s`的安全版本；

### 7.12 指针与函数

#### 7.12.0 指针作为函数形参

* 指针作为函数形参可以在不同函数之间传递变量
* 一维数组名作为函数形参，会退化为一级指针

```cpp
int f(int a[]);
int f(int a[10]);
int f(int *a);
```

1. 一级指针等价于一维数组
2. 二级指针等价于指针数组
3. 数组指针等价于二维数组

下面哪一个函数能接受处理任意行、任意列的int矩阵，row表示行数，col表示列数：

```cpp
void print(int a[row][col]);

void print(int a[][col], int row);

void print(int *a[], int row, int col);

void print(int a[][], int row, int col);
```

若有

```cpp
int a[4] = {1, 2, 3, 4};

int b[3] = {1, 2, 3};

int  c[3][4] =  {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

int *d[] = {c[0], c[1], c[2]};
```

1.以下选项不能作为实参调用函数`void f(int (*p)[4])`的是：

A. `&a`

B. `&b`

C. `&c[0]`

D. `c`

2.以下选项能作为实参调用函数`void f(int **)`的是：

A. `&a`

B. `&b`

C. `c`

D. `d`

#### 7.12.1 指针作为函数返回值

```cpp
int *f() {
    int a = 3;
    return &a;
}
```

* 不能返回局部非静态变量
* 返回外部传进来的指针

```cpp
int *f(int *p) {
    return p;
}
```

* 返回局部静态变量

```cpp
int *f() {
    static int a = 3;
    return &a;
}
```

* 返回全局变量
* 返回动态开辟的内存

```cpp
int *f() {
    int *p = new int(233);
    return p;
}
```

#### 7.12.2 函数指针

```cpp
int add(int a, int b) {
    return a+b;
}
int main(void) {
    int (*p)(int, int) = add;
    int (*p)(int, int) = &add;
    cout << p(1, 2) << endl;
    cout << (*p)(1, 2);
    return 0;
}
```

* 函数指针用于指向某一类函数
* typedef或using起别名

```cpp
typedef int (*FP)(int, int);
using FP = int (*)(int, int);
FP p = add;
```

设有语句

```cpp
int* f(int *p, int a) {return p + a;}
```

下面哪一个定义了返回值为int型指针的函数？

```cpp
int* (*fp1)(int*, int) = f;
```

```cpp
int* (*fp2[4])(int*, int) = {f};
```

```cpp
int* fun(int*(*fp)(int*, int)) {  
	return fp(nullptr, 3);   
}
```

### 7.13 动态分配内存 `new` 和 `delete`

* `new`返回一个指向开辟出的内存的指针，若开辟了一个数组，则是指向首元素的指针

下面哪一条语句是正确的？

```cpp
float &y = new float;
```

```cpp
float &y = *new float;
```

```cpp
float &y = new float[3];
```

```cpp
float &y = new *float;
```

* 若创建的是二维数组，则返回值类型为数组指针

```cpp
int (*p)[4] = new int[3][4];
```

* `new`出来的内存在堆上，不在当前函数的栈内存上，不会自动回收
* `delete`可以手动删除`new`出来的指针

```cpp
int *p = new int(3);
delete p;
int *q = new int[5];
delete[] q;
```

### 7.14 `const` 指针、`void` 指针、空指针、野指针

#### 7.14.0 `const` 指针

* `const`默认修饰左边，除非左边没有或者左边已经被修饰

```cpp
int const *p;
const int *p;
    
int * const p;
    
const int * const p;
int const * const p;
const int const * p;
int const const * p;
```

* const指针的重载问题：之前在函数重载中提到，const不能作为重载的依据，即一个形参变量是不是const修饰，不作为形参的区别，但是指针这里有例外

```cpp
void f(int *p);
void f(const int *p);
void f(int * const p);
```

```cpp
void f(int &a);
void f(const int &a);
```

#### 7.14.1 `void`指针

* void指针可指向任何类型的变量、函数
* 通过强制类型转换，void指针可以控制许多变量，进行通用性设计

#### 7.14.2 空指针和野指针

* 指针可以不指向任何地方，赋值为`nullptr`即可（或`NULL`）
* 野指针：被 `delete` 了的指针（这种也叫 悬垂指针）或者没有初始化赋值的局部变量指针，我们不知道它指向了哪里🤨

## 8. 引用

### 8.0 左值引用

* 引用就是别名
* 可以把引用**看作**功能阉割的指针，灵活性减小，但是使用更加方便了，但引用不是指针

```cpp
int a;
int& ra = a;
```

* 之后对`ra`的所有操作就相当于对`a`的操作
* `const`修饰的左值引用可以引用字面值或者右值表达式

```cpp
const int &ri = 4;
const int &ri = a + 4;
```

### 8.1 引用与函数

* 引用作为函数形参，可以传递变量
* 引用作为函数返回值，和指针作为函数返回值类似\`\`

```cpp
int &f1() {
    static int count = 1;
    return ++count;
}

int index;

int &f2() {
    return index;
}

int main() {
    f1() = 100;
    for(int i=0; i<5; i++)
        cout << f1() << " ";
    f2() = 100;
    int n = f2();
    cout << n << endl;
    f2() = 200;
    cout << index << endl;
    return 0;
}
```

* 返回动态开辟的内存的引用，注意用完之后`delete`

```cpp
int& f() {
    int *p = new int(3);
    return *p;
}

int main() {
    int &rf = f();
    cout << rf << endl;
    delete rf;
    return 0;
}
```

* 函数的引用

```cpp
int add(int a, int b) {
    return a+b;
}

int main() {
    int (&p)(int, int) = add;
    cout << p(2, 3) << endl;
    return 0;
}
```

* 函数引用可作为函数的返回值

### 8.2 引用与数组

* 和指针不同的是，数组的元素不能是引用
* 可以有数组的引用，但数组的引用不能作为函数的返回值

```cpp
int a[] = {1, 2, 3};
int (&ra)[3] = a;
cout << ra[1];
```

### 8.3 引用与指针

* 引用的指针就是被引用变量的指针

```cpp
int a, &ra = a, *p = &ra;
```

* 指针的引用

```cpp
int a, *p = &a;
int* &rp = p;
```

* 没有引用的引用，芝士右值引用

```cpp
int &&r = 3;
```

* 没有空引用

### 8.4 `&` 符号

* 到目前为止，已经接触了 & 的三种用法，注意不要混淆
  1. `&`在位运算中代表按位与
  2. `&`表示取地址操作
  3. `&`可以用来定义 左值引用

## 9. 链表

* 链表结点的结构：存值的变量，存后继的指针

```cpp
struct Node {
    int x;
    Node* next;
};
```

* 处理链表的函数中，通常都是会判断是否是头结点，并做特殊处理，或者判断头结点是否为空；
* 通常需要两个指针来处理链表，一个用来指向新的结点，一个用来指向链表的某个位置；
* 遍历链表时，通常会出现`p = p->next;`这样的语句；

1.已知一个单向链表结点的数据结构为：

```cpp
struct Node{
	int data;
	Node *next; 
};
```

函数`Node* AddNode (Node *head, int n)`的功能是：新建结点使之数据为n，将该结点处插入到head指向的单向链表中，放在数据与n差距最小的结点（如有多个最小差距结点，则考虑第一个）之后，并返回链头结点地址。若为空链，则新建结点直接作为头结点。

在程序空格位置填入合适的代码，完成`AddNode`函数。

```cpp
Node * AddNode(Node *head,int n){
	Node *p=new Node; //新建结点
	 /* (1)  */    ;
	p->next=NULL;
	if(head==NULL){  //若为空链
	 /* (2)  */    ;
	}
	Node *q;   //指向待查结点
	Node *mq=head;   //指向差距最小的结点
	for(q=head;q!=NULL;q=q->next)
		if(     /* (3)  */      )
		  mq=q;
	p->next=    /* (4)  */      ;
	mq->next=      /* (5)  */    ;
	return head;
}
```

2.在精准扶贫的大背景下，在政府和企业的帮助下，小明开了个养猪场。该养猪场有若干猪圈，我们用一个单向链表用于保存猪圈中猪的相关信息，其结点的数据结构为：

```cpp
struct Pig{
	double weight;
	Pig *next;
};
```

函数`double Sell(Pig *&head, double threshold, double price)`的功能是：将体重超过threshold斤的猪出笼，以price元/斤的价格售出并返回总售价。具体操作为：将链表中weight超过threshold的结点全部删除，同时根据price元/斤的价格计算并返回总售价。

在程序空格位置填入合适的代码，完成Sell函数。

```cpp
double Sell(Pig *&head, double threshold, double price){
	double total=0;
	Pig *p1,*p2=head;
	while(    /* (1) */    ){   //判断头结点是否要删除
		    /* (2) */    ;       //下面删除头结点
		total+=p2->weight;
		delete p2;
		p2=head;
	}
	if(head) {    //遍历链表，判断是否有非头结点要删除
	    for(p1=p2=head;p2;p1=p2,p2=p2->next)
		    if(p2->weight>threshold){   //删除非头结点的结点
			         /* (3) */     ;
			    total+=p2->weight;
			    delete p2;
			         /* (4) */     ;
	        }
    }
	/* (5) */    ;
} 
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://cpp.wyywiki.cn/2024.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
