# 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FJNRQ8evlhxBQvbAjRNhu%2Ftype.png?alt=media&#x26;token=26444f1e-f375-411f-aeb0-8a6d2e963709" 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FBcV3YS2B0Aw9vsph4iRR%2Fbuma.png?alt=media&#x26;token=77977db2-b5fb-4711-a273-0adc30aaf8b1" 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FBRQ8ltGxxbwt47oGJLH4%2F%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2.png?alt=media&#x26;token=aad8579d-a6c1-466d-ace8-19b09a425260" 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FsuEeepGqWr1wCMDXuGcX%2Ftn.png?alt=media&#x26;token=a69bb9c3-cd89-47bc-81d2-e3f576d13607" 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FvlGxjSTGazEVwejUr2rE%2F%E6%95%B0%E7%BB%84%E6%8C%87%E9%92%88.png?alt=media&#x26;token=f9df4489-d1bc-448b-bc03-ae922a27aaa4" 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="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FqAgXDBJ7AafW1TQs9VSz%2F%E5%AD%97%E7%AC%A6%E6%95%B0%E7%BB%84.png?alt=media&#x26;token=ad193190-c15a-4707-96f5-62a0b380c893" alt=""><figcaption></figcaption></figure>

<figure><img src="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2Fj5A606yN8RwSXu9iyr2y%2F%E7%A2%B0%E9%83%BD%E4%B8%8D%E8%83%BD%E7%A2%B0%E7%9A%84%E5%86%85%E5%AD%98.png?alt=media&#x26;token=0405b3f7-a29a-43c2-a4ac-ecb03262ccdb" alt=""><figcaption></figcaption></figure>

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

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

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

<figure><img src="https://443594442-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLSIQHnSiaefmd7OttLuo%2Fuploads%2FTocVwyFz6ctgEU23YNFm%2F%E5%AD%97%E7%AC%A6%E6%8C%87%E9%92%88.png?alt=media&#x26;token=6e2e68f1-0ac3-491f-9c1f-97e9bb656efb" 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) */    ;
} 
```
