Skip to content

全国信奥联赛C++训练营 学习笔记 ​

原视频BV:BV1f84y1Z7ce

2026/10/18日start to learn

一二节课略 ​

03 整数与字符 ​

整数类型 ​

  • win64和unix上long整数的占用空间不同,所以不要用long,用long long

基本运算 ​

  • 乘除法优先级高,支持括号优先级
  • 整数的除法是整除的意思 有浮点数参加才是正常除法
  • %是取余

字符类型 ​

  • 字符类型本质也是整数
  • 可以用整数给字符赋值,也可以把字符类型赋给整数
  • 字符的字面表达只支持ASCII字符,特殊字符需要转义
  • 字符的字面变量表达用单引号,不能包含汉字/多个字母
  • (类型)变量 强制转换类型
  • 写不出的字符需要转义,比如
    • \t 制表符
    • \n 换行符
    • \\ 反斜杠
    • ……

signed 与 unsigned ​

  • 默认signed,unsigned表示无符号
  • 它们并不改变整数的内存大小,但改变了取值范围,unsigned类型表示的最大值更大些
  • 表达的不同信息的个数不变
  • int 范围:-2^31~2^31-1(大约20亿)
  • unsigned int范围:0~2^32
  • char范围-2^7~2^7-1 -128~127
  • unsigned char范围:0~2^8 0~256

04 类型转换 ​

浮点类型 ​

|类型|占用字节数|有效数字位数|极值| |-|-|-| |float|4|6|约10^-38~10^38 |double|8|15|约10^-308~10^308

  • 浮点数并不精确,太大的数,太小的数有极限,并且有效数字位数有限制
  • 浮点的字面量表达法:
c++
float a=3.15F;//F明确类型为float,字面量默认是double
//👇自动转换
double b=300;
double C = 300.0;
doubled=1.2e-3;/*教程:e指数的表达法,表示绝对值很大或很小的数
我的理解:就是科学计数法,3e5就是3*10^5

自动转换与强制转换 ​

  • 规则 小的类型可自动升级为大的类型: char ==> short ==> int ==> long long ==> float ==> double
  • 如果相反方向,需要强制转换
  • 注意:强制转换的时候,可能会丢失信息,称为截断
c++
double a =3.15;
int b =(int)a;
cout<<b<< endl;

千万不要相等比较 ​

错误示范:直接用 == 比较浮点数:

c++
double a = 1.0;
double b = 0.1;
/* 错误示范:直接用 == 比较浮点数
 原因:0.1、0.9 无法用二进制浮点数精确表示,
 1.0 - 0.1 的实际结果不是精确的 0.9, 而是 0.9000000000000000222... 之类的近似值,
所以 (a - b == 0.9) 结果为 false(输出 0)。
*/
cout << (a - b == 0.9) << endl;  // 输出 0,比较不成立

/*正确的:
绝对值(a-b)<很小的数值
其意思是:a与b足够接近吗?*/
double a=1.0;
double b = 0.1;
cout<< (fabs(a-b -0.9)<1e-8);//差小于

四舍五入 ​

早期的c没有提供函数 c++99标准中有ceil floor round函数

c++
double a = 3.14159;
double b= (int)(a *1000+ 0.5) / 1000.0;
cout<< b<<endl;

cout<<round(a*1000)/1000.0<< endl;
函数名说明2.42.6-2.4-2.6
floor()不大于它的最大整数22-3-3
ceil()不小于它的最小整数33-2-2
roundO临近它的整数33-2-3

05 布尔逻辑 & IF ​

布尔逻辑 ​

逻辑运算符 ​
  • !
  • &&
  • ||
关系运算符 ​
  • ==, !=, > <, >=, <=

例子:

c++
bool a=5>3;

c++99标准中bool类型占用 1字节 的内存

IF ​

if语句基本格式 ​
c++
if(关系表达式) {
    当条件成立时需要执行的语句;
}
  • 特点: 条件成立(为真)时执行大括号内语句 条件不成立则跳过,执行后续语句
else、else if略过 ​

06 交换变量&三数排序 ​

题目略过 ​

宏替换/展开: ​

  • 宏最好不用,有更好的解决办法
c++
#include <iostream>
#include <cmath>
using namespace std;
#define SWAP(a,b) {int t=a; a=b; b=t;}//不用分号

int main(){
	int a,b;
	cin >> a >> b;
	if (a > b) SWAP (a,b)//不用分号
	cout<<a<<b;
}

临时变量 ​

  • 使用临时变量是,最好定义为语句块内的临时变量(生存期愈短愈好,夜长梦多)

07-指针!!! ​

  • C/C++ 里最重要的概念之一,不懂指针相当于没学。

指针的目的 ​

通过一个变量,间接地访问另一个变量。

  • 普通变量:直接存数据本身。
  • 指针变量:存另一个变量的地址,通过地址“间接”找到那个变量。
cpp
int a = 10;
int* p = &a;   // p 存的是 a 的地址
*p = 20;       // 通过 p 间接修改 a

指针的本质 ​

  • 指针本质上是一个整数 存内存地址编号。
cpp
double a = 12.5;
double* p = &a;
cout << p << endl;// 输出地址(十六进制)
cout << sizeof(p) << endl; // 64位系统通常是8

指针的含义 ​

指针的值 = 另一个数据的内存地址编号(虚拟地址)。

  • &a 取的是 a 的地址。
  • p = &a 让 p 指向 a。
  • *p 是“解引用”,表示“p 指向的那个变量”。

指针的语法 ​

1. 声明指针 ​

基本语法 ​
cpp
int* p;        // 指向 int 的指针(最推荐写法)
int *p;        // 等价,* 靠近变量名
int * p;       // 也等价
  • int* 是类型:一个指向 int 的指针。
同时声明多个 ​

注意:

cpp
int* a, b;   // a 是 int*,b 是 int!不是两个指针
int *a, *b;  // a、b 都是 int*

建议一行只声明一个指针

2. 重点!取地址 & / 解引用 * ​

cpp
int a = 10;
int* p = &a;   // & 取地址:p 存 a 的地址
cout << *p;    // * 解引用:得到 p 指向的值,即 10
*p = 20;       // 通过 p 修改 a
  • &a:取变量 a 的地址,类型是 int*。
  • *p:访问 p 指向的数据,可读可写。
  • & 和 * 是一对相反操作:*&a 就是 a。

3. 空指针 ​

cpp
int* p = nullptr;   // C++11 推荐
int* q = NULL;      // 老写法,NULL 通常是 0
int* r = 0;         // 也可以,但不推荐
  • 空指针不指向任何有效数据。
  • 不能解引用空指针:*p 是未定义行为(通常崩溃)。
使用前要进行判断 ​
cpp
if (p != nullptr) {}

4. 指针的类型必须匹配 ​

cpp
int a = 10;
double* p = &a;   // cross:类型不匹配
int* p = &a;      // tick
  • int* 只能指向 int,double* 只能指向 double。
  • 强行转换很危险,初学别用

5. 指针的算术运算 ​

cpp
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;      // 数组名退化为首元素地址,比如arr 退化为 &arr[0]

cout << *p;        // 1
p++;               // 指向下一个 int,地址 +sizeof(int)
cout << *p;        // 2

cout << *(p + 2);  // 4,等价于 p[2]
cout << p[2];      // 4,指针支持下标
  • p + n:地址增加 n * sizeof(*p) 字节。
  • p - q:两个同类型指针相减,得到元素个数(下标差,不是字节差)。
  • 指针算术只对同一数组内的指针有意义。

6. 指针与数组 ​

cpp
int arr[3] = {10, 20, 30};
int* p = arr;        // 等价于 &arr[0]

cout << p[1];        // 20
cout << *(p + 1);    // 20,等价
cout << arr[1];      // 20
  • 数组名在多数表达式里退化为指向首元素的指针。
  • arr[i] 等价于 *(arr + i),也等价于 *(p + i)、p[i]。
  • 但 sizeof(arr) 是数组总大小,sizeof(p) 是指针大小,不同。

7. 指针与函数 ​

指针作为参数 ​

cpp
void f(int* p) {
    *p = 100;   // 改 p 指向的数据
}

int main() {
    int a = 10;
    f(&a);      // 传地址
    cout << a;  // 100
}

指针作为返回值 ​

cpp
int* find(int* arr, int n, int target) {
    for (int i = 0; i < n; i++)
        if (arr[i] == target) return &arr[i];
    return nullptr;
}
  • 返回指针时,不要返回局部变量的地址(函数结束就失效)。
cpp
int* bad() {
    int x = 10;
    return &x;   // CROSS 危险:x 已销毁
}

函数指针(进阶) ​

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

int (*fp)(int, int) = add;   // fp 是函数指针
cout << fp(2, 3);            // 5
  • int (*fp)(int, int):指向“返回 int、参数两个 int”的函数的指针。
  • 注意括号:int* fp(int, int) 是“返回 int* 的函数”,不是函数指针。

8. 指针的指针(二级指针) ​

cpp
int a = 10;
int* p = &a;
int** pp = &p;    // pp 指向 p

cout << **pp;     // 10
  • int**:指向“int*”的指针。
  • 用于函数里修改实参指针本身:
cpp
void setNull(int** pp) {
    *pp = nullptr;
}

int main() {
    int a = 10;
    int* p = &a;
    setNull(&p);      // 传 p 的地址
    // 现在 p 是 nullptr
}

9. 指针的引用(C++ 特有,改实参指针更简洁) ​

cpp
void setNull(int*& p) {   // p 是 int* 的引用
    p = nullptr;
}

int main() {
    int a = 10;
    int* p = &a;
    setNull(p);    // 直接传 p
    // p 现在是 nullptr
}
  • int*&:指针的引用。
  • 比二级指针更直观,推荐 C++ 里用这个。

10. 动态内存(简单提) ​

cpp
int* p = new int(10);   // 分配一个 int,值为 10
cout << *p;             // 10
delete p;               // 释放

int* arr = new int[5];  // 分配数组
delete[] arr;           // 释放数组
  • new 配 delete,new[] 配 delete[]。
  • 现代 C++ 更推荐 std::unique_ptr / std::vector,少手动 new/delete。

11. 常见错误 ​

cpp
int* p;        // 未初始化,野指针
*p = 10;       // CROSS 未定义行为

int* p = nullptr;
*p = 10;       // CROSS 解引用空指针

int a;
int* p = &a;
p++;           // 指向未知内存,别乱用

12. 一句话语法总结 ​

T* p 声明指向 T 的指针;&x 取地址,*p 解引用;nullptr 是空指针;p+n 按 sizeof(T) 移动;数组名退化为首元素指针;arr[i] == *(arr+i);函数传指针是值传递(复制地址),改数据可见、改指针本身不可见;要改实参指针用 T** 或 T*&。

一些说法 ​

说法对不对说明
不同类型的指针变量占用内存空间大小一样对64 位系统上 int*、double*、char* 都是 8 字节,存的是地址
指针变量可以等于 0对int* p = nullptr; 或 p = 0; 表示空指针,不指向任何有效数据
指针++:把指针内的值加一个定值,与类型有关对p++ 不是加 1 字节,而是加 sizeof(*p) 字节。int* p++ 地址 +4,double* p++ 地址 +8

补充:

  • nullptr 是 C++11 的空指针常量,比 0 / NULL 更安全。
  • 空指针不能解引用(*p 会崩溃/未定义行为)。

例子 1 ​

cpp
#include <iostream>
using namespace std;

int main() {
    double a = 12.5;
    double* p = &a;

    cout << p << endl;         // 输出 a 的地址
    cout << sizeof(p) << endl; // 指针本身的大小,64 位通常是 8
    cout << *p << endl;        // 12.5,解引用
    return 0;
}

浅拷贝(指针赋值) ​

对指针的赋值,会造成多个指针指向同样的数据;共享数据,但不共享指针变量本身。

cpp
int a = 10;
int* p1 = &a;
int* p2 = p1;   // 复制地址,p1、p2 都指向 a

*p1 = 20;       // 通过 p1 改 a
cout << *p2;    // 20,p2 也看到变化

p1 = nullptr;   // 改的是 p1 自己
cout << *p2;    // 仍是 20,p2 不受影响

要点:

  • 赋值复制的是地址值。
  • 两个指针变量各自独立,但指向同一块数据。
  • 通过任一指针改数据,另一个能看到;改指针的指向,互不影响。

CSTDIO ​

尽量用cstdio,不用stdio.h
↑ 万能头:?

“万能头” ​

cpp
#include <bits/stdc++.h>
  • 这是 GCC/G++ 的非标准扩展头,一次性包含大部分标准库。
  • 方便竞赛/刷题,但不是 C++ 标准,MSVC 等编译器可能不支持。
  • 工程代码不建议用,编译变慢、可移植性差。

例子 2 ​

cpp
#include <iostream>
using namespace std;

#define SWAP(a, b) { double* t = (a); (a) = (b); (b) = t; }

int main() {
    double x, y, z;
    cin >> x >> y >> z;

    double* a = &x;
    double* b = &y;
    double* c = &z;

    // 通过交换指针的指向,让 a/b/c 按值从小到大排列
    if (*a > *b) SWAP(a, b);
    if (*b > *c) SWAP(b, c);
    if (*a > *b) SWAP(a, b);

    // 原数据 x/y/z 不动,只是指针指向变了
    cout << *a << " " << *b << " " << *c;
    return 0;
}

要点:

  • SWAP(a, b) 交换的是指针的指向,不是 x/y/z 的值。
  • 所以“原数据不动”,只是 a/b/c 指向了不同的变量。
  • 这是“通过指针间接访问、排序指针”的典型例子。
  • 注意宏要加括号,避免优先级问题

08-函数 ​

函数是什么 ​

  • 函数是一段代码。 调用函数导致去执行这段代码。 执行完毕,会返回到调用语句的下一句继续执行。 函数中还可以调用其它函数,甚至是调用自己。 这个工作要有条不紊,是通过栈来完成的

栈是什么 ​

  • 栈好比一个很细长的桶,或是“竖井” 只能在开口处,放入东西(push),或者拿出东西(pop)

  • LIFO规则,后进先出, (能沿着原路返回来)

###函数调用步骤

  1. 保存当前的执行断点(断点压栈,一会儿回来要从这里继续执行的)
  2. 把函数的变参压栈(相当于创建了新变量,其值从实参拷贝过来)
  3. 进入函数,执行代码
  4. 当执行到尾,或遇到return语句,则准备返回
  5. 参数出栈
  6. 恢复原来的断点(断点出栈)
  7. 继续执行调用函数之后的语句

重要定理 ​

  • 形参的改变不影响实参。 但,当参数为指针类型时,会造成影响的假象。

09-WHILE循环 ​

  • 条件成立执行
  • break跳出
  • continue跳出本轮

例子1 - 颠倒数
取位&删位 ​

思路 ​

  • cin>>x
  • 循环
    • 取得x的最后一位并输出
    • 设法删除x的最后一位
    • x没了?结束循环

代码 ​

c++
#include<bits/stdc++.h>
using namespace std;
int main(){
	int x;xin>>x;
	while(1){         //死循环,符合退出
		cout<<x%10;    //取最后一位
		x/=10;         //去最后一位
		if(x==0)break;//没了,退出
	}
}

WHILE 与 DO-WHILE ​

c++
//当条件成立才进入循环体
while(条件){
	//体(含条件变化)

// 循环体执行至少一次
do{
	//体(含条件变化)}
}while(条件);//注意,不要少了分号。

上一题用do-while其实更好:

例子2 - 颠倒数do-while写法 ​

c++
#include<bits/stdc++.h>
using namespace std;
int main(){
	int x;xin>>x;
	do{
		cout<<x%10;
		x/=10;
	}while(x!=0){
	}
}

100-FOR循环 ​

教程是以while为主for为辅 ​

####教程文案:
仅仅使用while循环就可以完成任何需要循环的逻辑。
但有些情况,使用 for 循环来表达会更自然、更易于理解。

###for循环特点

  • 循环次数固定
  • 内部运算依赖于循环变量

###小知识

cpp
for(int i...){
//Komii最帅
}
  • i在左花括号生成,在右花括号死亡,自动分配,自动释放
  • 循环变量只会被创建一次

###for语句语法

c++
for(初始化语句;循环条件,为真继续;每轮结束后执行){
}
  • break跳出
  • continue跳出本轮

无限循环:

c++
for(;;)

其他略过... ​

11-数组与串 ​

数组 ​

###数组是什么

  • 数组是连续多个同类型变量,只有一个名字。

###数组的特点

  • 从0开始,c++不检查越界,所以错误很隐蔽。
  • 通过下标可以访问数组

数组的语法 ​

####定义

c++
int a[3]//开辟空间
int b[3]={1,2,3}//初始化

####赋值

c++
a[0]=10;//第一个元素赋值

###栈中的数组,堆中的数组

3种常见的变量类型:局部,全局,堆 ​

其特征比较:

变量|备注|分配|释放|生存期 -|-|-----|----|----|-- 局部变量|定义在函数内,非static|int a;|自动出栈|从包含它的左大括号到右大括号 全局变量|定义在函数外|int a;|不用释放|与程序共存亡 堆变量|由指针指向该空间|int* p = new int;|delete p;|从new 到 delete 期

与普通变量类似,数组也可以在堆中分配,获得更长生存期,语法相仿

c++
int *p= new int[3]; // 数组是堆上分配的,但指向它的指针在栈里
p[0]=114514//分配完后正常使用
cout<<p[0];

delete [] p;

#####删数组要加方括号!
不写delete会造成内存泄露!

###串 ###串是什么 串实际上是char型的数组,只不过我们规定,它结尾的char值为0, 也就是说,空串也要分配1个char

串的语法 ​

####定义&赋值

c++
chars[10]="ha";
cout<<s[0]<<endl;
cout<<s[1]<<endl;
cout<<(int)s[2]<<endl;//'\0'这个字符是无法显示出来的

####修改&输出 如何手工变成"haha"?

c++
-skip-

s[2]="h";
s[3]="a";
s[4]=0//或s[4]="\0"

cout<<s;//自动输出 haha 不用循环输出

####例子1-反转串(指针)

cpp
……
int main(){
	chars[100];cin>>s;
	char* p=s;
	char* q=s;while(*q)q++;//如果到末尾(\0)停止
	while(p<q){
		char t=*p;
		*p=*q;
		*q=*t;
		p++;q--;
	}
	cout<<s<<end1;
}

12-二维数组 ​

###二维数组是什么

  • 二维数组可以表示数学上的矩阵
  • 二维数组是连续的存储区,存完第一行存第二行。。。

因此,给定了第一个元素的位置,可以算出任何行、列下标元素的位置。 反之亦然 公式:相对于第一个元素偏移=行号*宽度+列号 相反地:行号=偏移/宽度列号=偏移%宽度

###二维数组 ####定义

c++
int a[3][2];
int b[3][2]={{1,2,3},{4,5,6},{7,8,9}};

####其他类似python,跳过

###例子1-逐列填充 ####我的方法

c++
#include <bits/stdc++.h>
using namespace std;
int main(){
    int a[3][4];
	int t=1;
	for(int i=1;i<=3;i++){
		for(int j=1;j<=4;j++){
			a[i-1][j-1]=t++;//=执行完了后n++
		}
	}
	for(int i=1;i<=4;i++){
		for(int j=1;j<=3;j++){
			cout<<a[j-1][i-1]<<" ";
		}
		cout<<endl;
	}
}

####教程方法

c++
-skip-
int main()
	int a[3][4];
	int i=0, j=0;//行号,列号
	int n =1;
	do{
		a[i][j]=n++;
		i++;
		if(i==3){
			i=0;
			j++;
		}
	}while(j<4);
	
	for(int i=0; i<3;i++){
		for(int j=0;j<4;j++){
			cout<<setw(3)<<a[i][j];//指定宽度
		}cout<endl;
	}
	return O;
}

例子2-构造杨辉三角 ​

思路: 想象靠左对齐 得到a[il[j]= a[i-1][j-1]+ a[i-1][j],斜头顶+正头顶 deepseek:

c++
#include <iostream>
#include <iomanip>
using namespace std;

const int N = 10;
int main() {
    int a[N][N+1];

    // 构造杨辉三角
    for (int i = 0; i < N; i++) {
        a[i][0] = 1;      // 每行第一个为 1
        a[i][i] = 1;      // 每行最后一个为 1
        for (int j = 1; j < i; j++) {
            a[i][j] = a[i-1][j-1] + a[i-1][j];
        }
    }

    // 输出
    for (int i = 0; i < N; i++) {
        for (int j = 0; j <= i; j++) {
            cout << setw(4) << a[i][j];
        }
        cout << endl;
    }

    return 0;
}

13-函数和指针2 ​

###在函数中传出/入指针的作用 ####返回值为指针

  • c++的函数只能有一个返回值,当需要返回多个值的时候,就需要指针。 ####形参为指针
  • 一般有两个目的:
    • 实参类型较为庞大,按值传递会引发大面积拷贝,耗时
    • 希望函数的执行改变实参(因而可以作为返回值使用)

####参数为 指针指针 类型

  • 想修改指针的值

###例子1-多返回(同时返回商和余数

c++
#include<bits/stdc++.h>
using namespace std;

bool divmod(int a,int b,int* x,int* y){
	if(b==0)return false;//ERROE
	*x=a/b;
	*y=a%b;
	return true;
}
int main(){
	int a,b;cin>>a>>b;
	int x,y;
	bool t=divmod(a,b,&x,&y);

	if(t)cout<<x<<", "<<y;
	else cout<<"ERROR";
}

###例子2-把串复制为双倍(串可能很大 思路 想象调用一个函数,从堆上申请一个很大的空间,需要返回所分配的内存地址 本来可以利用返回值,但返回值可能被其它信息占用了。 需要先开辟足够空间,strlen(p)*2+1 然后内存拷贝

c++
#include<bits/stdc++.h>
using namespace std;
bool doubleit(const char p,char** buf){
	if(p==NULL || *p==0)return false;//空指针/空串无意义
	
	int n=strlen(p);//csting
	*buf=new char[2*n+1];//+1是因为还有\0
	memmcy(*buf,p,n);//前n个复制
	memcpy(*bur+n,p,n);//再复制一份
	
	*(bur+2*n)=0;// \0
	return true;
}
int main(){
	const char*p="hello NOIP";
	char*q;
	bool t=doubleit(p,&q);//要改变q,q又是*,所以形参应该是**
	cout<<q<<end;
	delete q;//别忘了删掉堆
	
	return 0;//课程说竞赛不写可能就0分了
}

14-函数的递归 ​