颜色主题
全国信奥联赛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^32char范围-2^7~2^7-1 -128~127unsigned 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.4 | 2.6 | -2.4 | -2.6 |
|---|---|---|---|---|---|
| floor() | 不大于它的最大整数 | 2 | 2 | -3 | -3 |
| ceil() | 不小于它的最小整数 | 3 | 3 | -2 | -2 |
| roundO | 临近它的整数 | 3 | 3 | -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; // tickint*只能指向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); // 5int (*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; // 10int**:指向“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规则,后进先出, (能沿着原路返回来)
###函数调用步骤
- 保存当前的执行断点(断点压栈,一会儿回来要从这里继续执行的)
- 把函数的变参压栈(相当于创建了新变量,其值从实参拷贝过来)
- 进入函数,执行代码
- 当执行到尾,或遇到return语句,则准备返回
- 参数出栈
- 恢复原来的断点(断点出栈)
- 继续执行调用函数之后的语句
重要定理
- 形参的改变不影响实参。 但,当参数为指针类型时,会造成影响的假象。
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分了
}