C++ / Practice Notes
C++ 语法练习笔记
整理自 VS/Test 与 CLion/Test* 下所有 test*.cpp:把源码注释润色成笔记,并保留关键代码块与易错点。
1. 基础类型、字符与运算
这一组练习从最基础的输入输出、整型、浮点型、字符型和布尔类型开始。float 字面量建议显式写 f,否则小数字面量默认按 double 处理;char 使用单引号,只能放单个字符;字符串可以用 C 风格字符数组,也可以用 C++ 的 string。
- float 是单精度,double 是双精度。实际输出精度还会受 cout 默认格式影响。
- char 在内存中本质上保存的是编码值,所以可以把 char 强转成 int 观察 ASCII 编码。
- bool 输出时 true 为 1,false 为 0;输入或赋值时,除 0 以外通常都按 true 处理。
- 整数相除仍然得到整数;小数不能做 % 取模运算。
#include <iostream>
#include <string>
using namespace std;
int main() {
float f1 = 3.14f; // 显式声明单精度
double f2 = 3e-2; // 科学计数法
char ch = 'a'; // 字符用单引号
cout << ch << endl;
cout << (int)ch << endl; // 查看 ASCII 编码
string text = "hello world";
bool flag = true;
cout << text << " " << flag << endl;
}
2. 条件、循环与跳转
控制流练习覆盖 if、else if、switch、while、do while、for、break、continue 和三目运算符。这里最重要的不是背语法,而是知道每种结构适合表达什么逻辑。
- if / else if 适合范围判断或复杂条件;switch 适合离散值分支。
- while 先判断再执行,do while 先执行一次再判断。
- for 的三个表达式分别负责初始化、循环条件和每轮结束后的更新。
- break 退出当前 switch 或最近一层循环;continue 跳过本轮循环剩余语句。
- 三目运算符适合简单二选一,不适合塞太复杂的逻辑。
int score = 0;
cin >> score;
if (score > 8) {
cout << "A" << endl;
} else if (score > 5) {
cout << "B" << endl;
} else {
cout << "C" << endl;
}
for (int i = 1; i <= 9; ++i) {
for (int j = 1; j <= i; ++j) {
cout << j << "*" << i << "=" << i * j << " ";
}
cout << endl;
}
水仙花数、猜数字、敲桌子、九九乘法表这些小题都属于控制流练习:先把边界条件想清楚,再决定循环退出条件。
3. 数组、函数与分文件
数组练习强调两个点:数组是一段连续内存,数组名经常可以当作首元素地址使用;但数组传入函数后会退化为指针,函数内部不能再用 sizeof 自动得到原数组长度。
- 一维数组下标从 0 开始,未显式赋值的剩余元素会被初始化为 0。
- 二维数组可以按行列理解,本质上仍然是连续存储。
- 冒泡排序、数组反转这类题目适合练习下标和边界。
- 函数声明可以写多次,函数定义只能有一次;函数写在 main 后面时,需要提前声明。
- 分文件编写时,声明放 .h,定义放 .cpp,使用处 include 头文件。
void bubbleSort(int arr[], int len) {
for (int i = 0; i < len - 1; ++i) {
for (int j = 0; j < len - i - 1; ++j) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
int arr[] = {4, 2, 8, 0, 5};
int len = sizeof(arr) / sizeof(arr[0]);
bubbleSort(arr, len);
注意:不要在函数里写 sizeof(arr) / sizeof(int) 来求传入数组长度,因为此时 arr 已经是指针。长度应该由调用者一并传入。
4. 指针、引用与内存分区
指针练习从取地址、解引用开始,逐步引出空指针、野指针、const 修饰指针、指针与数组、地址传递。引用练习则强调“别名”这个核心概念。
- p 表示指针变量本身,*p 表示指针指向的对象。
- 空指针可以用来初始化,但不能解引用访问。
- 野指针指向非法或已经释放的内存,应尽量避免。
- const int* p 是常量指针:指向可变,指向的值不可通过 p 修改。
- int* const p 是指针常量:指向不可变,指向的值可修改。
- 引用必须初始化,并且初始化后不能再绑定到别的对象。
int a = 10;
int* p = &a;
*p = 100; // 修改 a
int& ref = a; // ref 是 a 的别名
ref = 200; // 仍然是在修改 a
void swapByRef(int& x, int& y) {
int temp = x;
x = y;
y = temp;
}
内存分区练习把程序运行时分成代码区、全局区、栈区和堆区。局部变量在栈上,函数结束后自动释放;new 在堆区开辟空间,需要 delete / delete[] 释放。不要返回局部变量地址或局部变量引用。
int* makeValue() {
int* p = new int(10); // 堆区开辟
return p;
}
int* p = makeValue();
cout << *p << endl;
delete p;
p = nullptr;
5. 结构体与封装
结构体用于把不同类型的数据组合到一个自定义类型里。结构体变量用 . 访问成员,结构体指针用 -> 访问成员。结构体可以嵌套,也可以作为数组元素或函数参数。
- 值传递会复制整个结构体,结构体较大时成本高。
- 地址传递可以减少复制,但可能误修改实参。
- const 指针或 const 引用适合只读访问,能防止误操作。
- struct 默认 public,class 默认 private。
struct Student {
string name;
int age;
int score;
};
void printStudent(const Student& s) {
cout << s.name << " " << s.age << " " << s.score << endl;
}
Student stu{"Alice", 18, 95};
printStudent(stu);
类的封装把属性和行为放在一起,并通过 public、protected、private 控制访问权限。成员属性设为 private 后,可以通过 setter 检查传入数据的合法性。
6. 构造、析构、深拷贝与 this
构造函数负责对象初始化,析构函数负责对象销毁前的清理。构造函数没有返回值,函数名与类名相同,可以重载;析构函数没有参数,不能重载,只会调用一次。
- 无参构造也叫默认构造。
- 有参构造用于创建对象时传入初始值。
- 拷贝构造通常写成 ClassName(const ClassName& other)。
- object o(); 会被编译器理解成函数声明,不是创建对象。
- 初始化列表更适合初始化成员对象和 const 成员。
- 静态成员属于类本身,所有对象共享一份数据。
class Person {
public:
Person(int age) : m_age(age) {}
Person(const Person& other) : m_age(other.m_age) {}
~Person() {}
Person& addAge(int value) {
this->m_age += value;
return *this;
}
private:
int m_age;
};
如果类里有堆区资源,默认拷贝构造和默认赋值通常只是浅拷贝,多个对象可能指向同一块堆内存,析构时会重复释放。解决方式是自己写深拷贝:重新申请堆区空间,再复制内容。
7. 友元、运算符重载与继承多态
友元 friend 可以让全局函数、类或成员函数访问另一个类的私有成员。它能解决封装边界下的某些访问需求,但不应滥用。
运算符重载是给已有运算符定义新的行为,使它适配自定义类型。+ 可以写成成员函数或全局函数;<< 通常必须写成全局函数,因为左操作数是 ostream。赋值运算符一般只能写成成员函数,并且要处理堆区资源的深拷贝。
class Box {
friend ostream& operator<<(ostream& out, const Box& box);
public:
Box(int value = 0) : value(value) {}
Box operator+(const Box& other) const {
return Box(value + other.value);
}
private:
int value;
};
ostream& operator<<(ostream& out, const Box& box) {
out << box.value;
return out; // 支持 cout << a << b 的链式调用
}
继承用于抽取共性,派生类继承基类内容。public / protected / private 继承会影响基类成员在派生类中的可见性。构造顺序是先父类后子类,析构顺序相反。
多态分为静态多态和动态多态。函数重载、运算符重载属于静态多态;继承配合虚函数属于动态多态。父类指针或引用指向子类对象时,如果调用虚函数,就会在运行期绑定到子类实现。
class Animal {
public:
virtual void speak() = 0; // 纯虚函数,Animal 成为抽象类
virtual ~Animal() = default; // 父类析构建议写 virtual
};
class Cat : public Animal {
public:
void speak() override {
cout << "cat" << endl;
}
};
void doSpeak(Animal& animal) {
animal.speak();
}
8. 文件读写
文件读写需要包含 fstream。ofstream 写文件,ifstream 读文件,fstream 可读可写。文本文件按字符保存,二进制文件按内存字节保存。
- open 失败时要检查 is_open。
- 路径里的反斜杠需要写成 \。
- 多种打开方式可以用 | 组合,例如 ios::in | ios::binary。
- 二进制直接写对象时,不建议写入包含 string、vector 或动态内存成员的对象。
#include <fstream>
ofstream ofs;
ofs.open("note.txt", ios::out);
ofs << "hello file" << endl;
ofs.close();
ifstream ifs("note.txt", ios::in);
string line;
while (getline(ifs, line)) {
cout << line << endl;
}
ifs.close();
9. 模板与泛型编程
模板是 C++ 泛型编程的基础,用一个虚拟类型代表未来才确定的真实类型。函数模板可以自动类型推导,也可以显式指定类型;类模板没有自动类型推导,创建对象时必须显式给出模板参数。
- 模板声明和它修饰的函数/类之间不能插入其他语句。
- 自动类型推导要求 T 推导结果一致。
- 普通函数和函数模板同名时,普通函数可用则优先调用普通函数。
- 模板可以重载,也可以为特定类型做具体化。
- 类模板成员函数通常在调用时才生成,分文件编写容易链接不到;常见做法是写到 .hpp。
template <typename T>
void mySwap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
template <typename T1, typename T2 = int>
class PairBox {
public:
PairBox(T1 first, T2 second) : first(first), second(second) {}
private:
T1 first;
T2 second;
};
10. STL、string、lambda 与常用算法
STL 可以从容器、算法、迭代器三条线理解。容器保存数据,算法处理数据,迭代器把二者连接起来。vector、deque、string、算法函数和函数对象都是后续写项目会频繁用到的东西。
- vector 是单端动态数组,支持随机访问;扩容时通常会申请更大空间、拷贝旧数据、释放旧空间。
- deque 是双端数组,头尾插入删除更方便,但随机访问通常不如 vector 简洁直接。
- string 本质上是管理 char* 的类,提供构造、赋值、追加、查找、替换、比较、子串等操作。
- lambda 是匿名函数,语法为 [捕获列表](参数) mutable -> 返回类型 { 函数体 }。
- 函数对象是重载了 operator() 的类对象;返回 bool 的函数对象称为谓词。
- 常用算法主要在 algorithm、functional、numeric 中。
#include <algorithm>
#include <vector>
using namespace std;
vector<int> nums{4, 1, 7, 2};
sort(nums.begin(), nums.end(), [](int a, int b) {
return a < b;
});
for_each(nums.begin(), nums.end(), [](int value) {
cout << value << " ";
});
常用算法可以按用途记:遍历 for_each,转换 transform,查找 find / find_if,统计 count / count_if,排序 sort,反转 reverse,拷贝 copy,替换 replace / replace_if,数值 accumulate,集合 set_intersection / set_union / set_difference。
整理结论
这些 test*.cpp 的练习路线很完整:先从基础语法建立手感,再进入函数、数组、指针和结构体,随后过渡到面向对象、内存管理、模板和 STL。后续如果继续整理,最值得做的是把每个小练习改成“题目、知识点、易错点、改进版代码”四段式,这样复习效率会更高。