C++ 基础与核心编程
C++ 基础
sizeof 和 strlen 的区别
strlen 是 C 语言中统计字符串长度的关键字,其统计遇到的第一个 \0 前面的有效字符(不论显示还是隐式)。sizeof 统计占据的所有内存字节数,包含显示 \0 和隐式 \0。
三目运算符
表达式 1 ? 表达式 2 : 表达式 3;
一维数组名作用
- 统计整个数组的字节数;
- 数组元素的首地址;
- 数组地址。
数组首地址与数组地址在值上是相等的,但是两者存在本质的区别,如下面代码中的解释说明:
系统梳理 C++ 基础知识与核心编程概念。内容包括 sizeof 与 strlen 的区别、指针与引用的本质差异、空指针与野指针的处理、const/volatile/static 关键字用法、内存分配机制对比。重点解析面向对象三大特性:封装(类结构、构造函数/析构函数、深浅拷贝、this 指针、友元)、继承(访问权限、构造析构顺序、菱形继承虚继承)及多态(静态/动态多态、虚函数)。最后简要介绍 STL 容器、算法、迭代器等组件。适合作为 C++ 学习总结或面试复习资料。
strlen 是 C 语言中统计字符串长度的关键字,其统计遇到的第一个 \0 前面的有效字符(不论显示还是隐式)。sizeof 统计占据的所有内存字节数,包含显示 \0 和隐式 \0。
表达式 1 ? 表达式 2 : 表达式 3;
数组首地址与数组地址在值上是相等的,但是两者存在本质的区别,如下面代码中的解释说明:
int arr[3] = {10, 20, 30};
// 1. 数组名:统计数组字节大小
cout << sizeof(arr); // 输出 12(3 个 int,每个 4 字节)
cout << sizeof(arr[0]); // 输出 4(首元素的大小)
// 2. 数组地址和数组元素的首地址
// arr 和&arr 两者在值上是相等的,但是本质不同
// arr(首元素地址)的类型是 int*(指向 int 的指针),步长是 sizeof(int)=4 字节。
// &arr(整个数组地址)的类型是 int (*)[3](指向 int[3] 数组的指针),步长是 sizeof(int[3])=12 字节
cout << "&arr(整个数组地址):" << &arr << endl;
cout << "arr(首元素地址):" << arr << endl;
int[2][3],它是'包含 2 个元素的数组',每个元素又是'包含 3 个 int 的数组';arr 会隐式转换为'指向其第一个一维子数组的指针',类型是 int (*)[3]。&数组名int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
// 1. 数组名:统计整个二位数组字节大小
cout << sizeof(arr); // 输出 24(2 个子数组 × 3 个 int × 4 字节/int)
// 2. 数组名:指向一维数组
int (*p)[3] = arr; // 等价于 p = &arr[0](arr[0] 是第一个一维子数组)
cout << (*p)[0]; // 输出 1(访问第一个子数组的第 0 个元素)
cout << (*(p+1))[0]; // 输出 4(p+1 指向第二个子数组,步长是 sizeof(int[3])=12 字节)
// 3. 整个二位数组地址:&数组名
int (*p_arr)[2][3] = &arr;
// 4. 数组名 + 下标(如 arr[0])—— 是'一维数组的数组名'
int* p_elem = arr[0]; // 等价于 p_elem = &arr[0][0]
cout << *p_elem; // 输出 1
函数的声明可以有多次,但是函数的定义只能有一次。
一定切记指针本身就是变量,存储某个变量的地址,所以存在指向指针的指针;但是引用表示对某个变量起别名,内存不会再单独开辟一个空间,与引用的变量共享同一块内存空间。引用必须初始化,引用的实质相当于指向指针常量,指针的指向不会改变,但是值可以修改。
int* const ptr = &value;const int *p = &a;class A { void func() const; }; --> const 成员函数内部对于变量和值,对于变量不能修改(只读操作);对于函数只能调用同意类下的 const 成员函数;const 类只能调用它的 const 成员函数。int *ptr_arr[5];int (*arr_ptr)[5];int (*func_ptr)(int, int);int *create_array(int size);struct 结构体名 结构体变量; --> struct teacher t1;struct 结构体名 结构体变量 = {成员 1 的值,成员 2 的值};struct 结构体名 {} 结构体变量;CPU 访问内存时,并不是逐个字节读取,而是按字长(Word Size) 读取(比如 32 位 CPU 字长是 4 字节,64 位是 8 字节)。如果数据的起始地址是其自身大小的整数倍,CPU 可以一次读取完成,效率最高;否则就需要多次读取并拼接,效率降低。
写在前面:对于下面两个结构体,虽然结构体中的内容不同,但是由于结构体中的成员变量顺序不同,所以结构体大小也不同。Unoptimized 中 char 后直接 int,需要填充 3 字节;而 Optimized 中 char 后接 short,仅需填充 1 字节。
我的理解:看第一个成员变量后面紧随的变量类型。
#include <stdio.h>
// 示例 1:未优化的成员顺序(有填充字节)
struct Unoptimized {
char a; // 1 字节
int b; // 4 字节
short c;// 2 字节
};
// 示例 2:优化后的成员顺序(减少填充)
struct Optimized {
char a; // 1 字节
short c;// 2 字节
int b; // 4 字节
};
int main() {
printf("Unoptimized size: %zu bytes\n", sizeof(struct Unoptimized));
printf("Optimized size: %zu bytes\n", sizeof(struct Optimized));
return 0;
}
代码输出:
Unoptimized size: 12 bytes
Optimized size: 8 bytes
内部解析:
char a:占用地址 0(1 字节),地址 1-3 填充 3 字节(为了让 int b 的起始地址是 4 的倍数)。int b:占用地址 4-7(4 字节)。short c:占用地址 8-9(2 字节),地址 10-11 填充 2 字节(整体大小需是最大成员 4 字节的整数倍)。char a:占用地址 0(1 字节),地址 1 填充 1 字节(让 short c 起始地址是 2 的倍数)。short c:占用地址 2-3(2 字节)。int b:占用地址 4-7(4 字节)。define 简单理解只是无脑替换,不进行类型检查,define 定义的宏常量替换后不会占内存;const 会进行类型检查,const 定义的常量会占据内存;new/delete 是 C++ 的运算符,malloc/free 是 C 语言的函数,二者都用于动态内存管理;new 按对象类型自动计算对应内存空间(如 new int 分配 4 字节);malloc 需要指定字节数(如 malloc(4));int*),无需强制类型转换;malloc 返回 void*,必须手动强制类型转换。define 只是简单的文本替换,不涉及任何语法检查,工作阶段处于编译之前;typedef 是关键字,其作用为起别名,单纯从这个角度来讲类似于引用,在编译阶段起作用;类默认访问和继承是 private,结构体默认访问和继承是 public。
公有继承、私有继承、保护继承:

默认情况下,C++ 编译器至少给一个类添加 3 个函数:
构造函数调用规则如下:如果用户定义有参构造函数,C++ 不再提供默认无参构造,但是会提供默认拷贝构造;如果用户定义拷贝构造函数,C++ 不会再提供其他构造函数;
主要作用在于对象销毁前系统自动调用,执行一些内存清理工作。构造函数和析构函数如下所示:
#include <iostream>
#include <cstring>
using namespace std;
// 定义 Person 类,包含构造、拷贝构造、析构函数
class Person {
private:
string name; // 姓名
int age; // 年龄
public:
// 1. 无参构造函数(默认构造)
// 如果不手动定义,编译器会自动生成空的无参构造
Person() {
cout << "调用【无参构造函数】" << endl;
name = "未知";
age = 0;
}
// 2. 有参构造函数
// 自定义参数初始化成员变量
Person(string n, int a) {
cout << "调用【有参构造函数】" << endl;
name = n;
age = a;
}
// 3. 拷贝构造函数
// 用已有对象初始化新对象,参数必须是 const 引用(const 防止修改原对象,引用避免无限递归)
Person(const Person& p) {
cout << "调用【拷贝构造函数】" << endl;
// 把原对象 p 的成员值复制给新对象
name = p.name;
age = p.age;
}
// 4. 析构函数
// 对象销毁时自动调用,用于释放资源(如堆内存、文件句柄等)
// 无参数、无返回值,名称是~类名
~Person() {
cout << "调用【析构函数】,销毁对象:" << name << endl;
}
// 成员函数:打印对象信息,方便验证
void showInfo() {
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
// 测试函数:验证拷贝构造的调用场景(值传递参数)
void testCopy(Person p) {
p.showInfo();
}
int main() {
// ========== 1. 调用无参构造 ==========
cout << "--- 创建无参对象 p1 ---" << endl;
Person p1;
p1.showInfo();
// ========== 2. 调用有参构造 ==========
cout << "\n--- 创建有参对象 p2 ---" << endl;
Person p2("张三", 20);
p2.showInfo();
// ========== 3. 调用拷贝构造 ==========
cout << "\n--- 用 p2 拷贝创建 p3 ---" << endl;
Person p3 = p2; // 场景 1:用已有对象初始化新对象
p3.showInfo();
cout << "\n--- 函数参数值传递调用拷贝构造 ---" << endl;
testCopy(p2); // 场景 2:函数参数为值传递时,拷贝构造新对象
// ========== 4. 析构函数调用 ==========
// 程序结束时,栈上的对象会按'先创建后销毁'的顺序调用析构
cout << "\n--- 程序结束,对象开始销毁 ---" << endl;
return 0;
}
int, float, char 这样的基本数据类型,浅拷贝会创建一个新的、独立的副本;对于像指针、引用这样的复杂类型,浅拷贝只会复制指针本身,而不会复制指针指向的内容。结果就是,新旧两个对象会共享同一块内存资源。在 C++ 中成员变量和成员函数是分开存储,只有非静态成员变量才会占据类对象的内存空间,静态成员变量和函数与常规成员函数不会占用类对象的内存空间。
写在前面:常见用法解决命名冲突、返回对象本身实现链式调用,本质是让成员函数'知道操作哪个对象'。
this 指针是 C++ 编译器自动为每个非静态成员函数添加的一个隐藏的、常量指针(const 指针),它指向调用该成员函数的那个对象本身。简单说:谁调用函数,this 就指向谁。return *this,如代码 2;类名* const:指针本身不能被修改(不能让 this 指向其他对象),但指向的对象内容可以改;(指针常量)this 指针(静态成员函数属于类,不依赖对象,因此没有 this)。this 是 NULL,this->age 等价于 NULL->age,非法访问内存):代码 1:
#include <iostream>
using namespace std;
class Person {
public:
int age;
void setAge(int age) {
// this->age:指向当前对象的成员变量 age
// age:函数的形参 age
this->age = age;
}
void showAge() {
// 即使没有命名冲突,this 也在默默工作
// 等价于 cout << this->age << endl;
cout << "年龄:" << age << endl;
}
};
int main() {
Person p1, p2;
// 调用 p1 的 setAge,this 指向 p1
p1.setAge(18);
// 调用 p2 的 setAge,this 指向 p2
p2.setAge(20);
p1.showAge(); // 输出:18(this 指向 p1)
p2.showAge(); // 输出:20(this 指向 p2)
return 0;
}
代码 2:
#include <iostream>
using namespace std;
class Person {
public:
int age;
// 返回对象本身的引用,支持链式调用
Person& addAge(int num) {
this->age += num;
return *this; // *this 就是当前对象本身
}
};
int main() {
Person p;
p.age = 10;
// 链式调用:连续调用 addAge
p.addAge(5).addAge(3);
cout << p.age << endl; // 输出:18(10+5+3)
return 0;
}
代码 3:
void Person::showAddr() {
cout << "当前对象的地址:" << this << endl; // 输出对象的内存地址
cout << "当前对象的年龄:" << (*this).age << endl; // *this 等价于对象本身
}
代码 4:
class Test {
public:
void func1() {
cout << "无成员变量访问" << endl;
}
void func2() {
cout << this->age << endl;
}
int age;
};
int main() {
Test* p = NULL;
p->func1(); // 不会崩溃(输出:无成员变量访问)
// p->func2(); // 崩溃!NULL 指针访问成员变量
return 0;
}
总结:只要看见 friend 关键字,那么这个函数就可以访问引入它的类的私有成员。
public、protect、private(上面已经介绍过)
子类继承父类时,继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
何为菱形继承,可以简单理解为,两个父类继承同一个爷爷,孙子类又继承两个父类,继承父类两份相同的数据,会导致资源浪费以及毫无意义,利用虚继承可以解决菱形继承问题,当两个派生类继承同一个基类时,采用虚继承。
virtual)+ 继承 + 父类指针 / 引用指向子类对象,代码 2;代码 1:
#include <iostream>
using namespace std;
// 函数重载:同一个函数名,参数列表不同(静态多态)
class Calculator {
public:
// 计算两个 int 的和
int add(int a, int b) {
return a + b;
}
// 计算三个 int 的和(参数个数不同)
int add(int a, int b, int c) {
return a + b + c;
}
// 计算两个 double 的和(参数类型不同)
double add(double a, double b) {
return a + b;
}
};
int main() {
Calculator calc;
// 编译时就确定调用哪个 add 函数
cout << calc.add(1, 2) << endl; // 调用 2 个 int 参数的 add,输出 3
cout << calc.add(1, 2, 3) << endl; // 调用 3 个 int 参数的 add,输出 6
cout << calc.add(1.5, 2.5) << endl; // 调用 2 个 double 参数的 add,输出 4.0
return 0;
}
代码 2:
#include <iostream>
using namespace std;
// 父类:动物
class Animal {
public:
// 虚函数:动物叫(核心,开启动态多态)
virtual void makeSound() {
cout << "未知动物的叫声" << endl;
}
// 析构函数也建议设为虚函数(避免子类析构不执行)
virtual ~Animal() {}
};
// 子类:猫
class Cat : public Animal {
public:
// 重写父类的虚函数(override 可选,建议加,编译器会检查重写是否正确)
void makeSound() override {
cout << "喵喵喵" << endl;
}
};
// 子类:狗
class Dog : public Animal {
public:
void makeSound() override {
cout << "汪汪汪" << endl;
}
};
// 统一的接口函数(接收父类引用)
void animalCry(Animal &animal) {
animal.makeSound(); // 运行时确定调用哪个子类的函数
}
int main() {
Cat cat;
Dog dog;
// 父类引用指向子类对象,调用不同的 makeSound
animalCry(cat); // 输出:喵喵喵
animalCry(dog); // 输出:汪汪汪
// 父类指针指向子类对象(另一种方式)
Animal *p1 = &cat;
p1->makeSound(); // 输出:喵喵喵
Animal *p2 = &dog;
p2->makeSound(); // 输出:汪汪汪
return 0;
}
virtual 关键字修饰的、有函数体的成员函数,目的是允许子类重写(override),从而实现动态多态。virtual 修饰、没有函数体,且以 = 0 结尾的成员函数,目的是强制子类必须重写该函数,同时将包含它的类变为抽象类,抽象类不能实例化对象(无法创建 Animal a; 这样的对象)。STL (Standard Template Library, 标准模板库);从广义上分为容器(container) 算法(algorithm) 迭代器(iterator),容器和算法之间通过迭代器进行无缝连接。
STL 大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online