c++随笔记录
目录
一 c++基础
1.sizeof和strlen的区别
strlen是C语言中统计字符串长度的关键字,其统计遇到的第一个\0前面的有效字符(不论显示还是隐式),sizeof统计占据的所有的内存字节数,包含显示\0和隐式\0。
2.三目运算符
表达式1?表达式2:表达式3;
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; 4.二维数组名作用
- 统计整个数组的字节数;
- 数组名作为 “指向一维数组的指针”(默认隐式转换,这里的维度看行数)。例如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; // 输出15.函数的声明
函数的声明可以有多次,但是函数的定义只能有一次。
6指针和引用的区别
一定切记指针本身就是变量,存储某个变量的地址,所以存在指向指针的指针;但是引用表示对某个变量起别名,内存不会再单独开辟一个空间,与引用的变量共享同一块内存空间,引用必须初始化,引用的实质相当于指向指针常量,指针的指向不会改变,但是值可以修改;
7.空指针和野指针
- 空指针:主要用于对指针进行初始化,空指针指向的内存空间是不允许被访问的(内存编号0 ~255为系统占用内存,不允许用户访问);
- 野指针:简单理解野指针就是指向无效内存地址的指针变量,其产生原因如下:
1)指针被释放后未置空;
2)指针未进行初始化;
3)指针指向局部变量;
8.const关键字
- 指针常量:const修饰p;int* const ptr = &value;
- 常量指针:const修饰*p;const int *p = &a;
- class A { void func() const; };-->const成员函数内部对于变量和值,对于变量不能修改(只读操作);对于函数只能调用同意类下的const成员函数;const类只能调用它的const成员函数。
9.指针和数组
- 指针数组:数组里面的每个元素都是指针;int *ptr_arr[5];
- 数组指针:指针指向数组;int (*arr_ptr)[5];
10.指针和函数
- 函数指针:指向函数的指针;int (*func_ptr)(int, int);
- 指针函数:函数的返回值是指针;int *create_array(int size);
11.结构体
- 结构体变量的定义方式:
1)struct 结构体名 结构体变量;-->struct teacher t1;
2) struct 结构体名 结构体变量 = {成员1的值,成员2的值};
3)struct 结构体名 {} 结构体变量;
- 结构体作为函数参数
传递方式有两种: 值传递/地址传递
特殊情况:结构体作为函数参数时,与const结合;(防止值被修改)
结构体实例化应用:当定义数据协议时,可以将数据定义为一个结构体去定义和解析数据。
结构体默认的访问和继承权限都是public,而类全是private;
结构体对齐面试常考知识点
CPU 访问内存时,并不是逐个字节读取,而是按字长(Word Size) 读取(比如 32 位 CPU 字长是 4 字节,64 位是 8 字节)。如果数据的起始地址是其自身大小的整数倍,CPU 可以一次读取完成,效率最高;否则就需要多次读取并拼接,效率降低。
结构体的总大小:必须是其最大成员类型大小的整数倍(不足则补填充字节)
成员对齐:每个成员的起始地址必须是其自身大小的整数倍(比如 char 占 1 字节,无限制;int 占 4 字节,起始地址需是 4 的倍数)。
写在前面:对于下面两个结构体,虽然结构体中的内容不同,但是由于结构体中的成员变量顺序不同,所以结构体大小也不同。
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 内部解析: struct Unoptimized: char a:占用地址 0(1 字节),地址 1-3 填充 3 字节(为了让 int b 的起始地址是 4 的倍数)。 int b:占用地址 4-7(4 字节)。 short c:占用地址 8-9(2 字节),地址 10-11 填充 2 字节(整体大小需是最大成员 4 字节的整数倍)。 总大小:1+3+4+2+2 = 12 字节。 struct Optimized: char a:占用地址 0(1 字节),地址 1 填充 1 字节(让 short c 起始地址是 2 的倍数)。 short c:占用地址 2-3(2 字节)。 int b:占用地址 4-7(4 字节)。 总大小:1+1+2+4 = 8 字节(刚好是最大成员 4 字节的 2 倍)。12.volatile关键字
- 防止编译器优化,告诉代码从内存去读取值,而不是通过寄存器的缓存去读取值。
- 资源共享:中断与主程序/多线程之间共享数据。
13.static关键字
- 静态变量:必须初始化;类内声明,类外初始化;作用周期贯穿整个程序,但作用范围只限局部范围内;属于类,而不是属于某个具体的对象;
- 静态函数:无需创建对象,可直接通过类名访问;属于类,而不是属于某个具体的对象;
14.define与const区别
- define简单理解只是无脑替换,不进行类型检查,define定义的宏常量替换后不会占内存;
- const会进行类型检查,const定义的常量会占据内存;
15.new/delete与malloc/free区别
new/delete是 C++ 的运算符,malloc/free是 C 语言的函数,二者都用于动态内存管理;- new按对象类型自动计算对应内存空间(如
new int分配 4 字节);malloc需要指定字节数(如malloc(4)); - new直接返回对应类型指针(如
int*),无需强制类型转换;malloc返回void*,必须手动强制类型转换。
16.define与typedef区别
- define只是简单的文本替换,不涉及任何语法检查,工作阶段处于编译之前;
- typedef是关键字,其作用为起别名,单纯从这个角度来讲类似于引用,在编译阶段起作用;
二 c++核心编程(封装、继承、多态)
2.1封装-类
1.类和结构体区别
类默认访问和继承是private,结构体默认访问和继承是public。
2.继承
公有继承、私有继承、保护继承:
公有继承:父类中的公有(public)和保护(protected)属性到子类中还是公有和保护属性;父类中的私有成员也会被子类继承,占用子类对象的内存空间,但子类完全无法直接访问这些私有成员。
保护继承:父类中的公有和保护属性到子类中变为保护属性;父类中的私有成员也会被子类继承,占用子类对象的内存空间,但子类完全无法直接访问这些私有成员。
私有属性:父类中的公有和保护属性到子类中变为私有属性;父类中的私有成员也会被子类继承,占用子类对象的内存空间,但子类完全无法直接访问这些私有成员。

3.构造函数
- 主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用;
- 构造函数与类同名;
- 具体可分为无参构造、有参构造和拷贝构造;
4.构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数:
1.默认构造函数(无参,函数体为空) 2.默认析构函数(无参,函数体为空) 3.默认拷贝构造函数,对属性进行值拷贝构造函数调用规则如下:如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造;如果用户定义拷贝构造函数,c++不会再提供其他构造函数;
5.析构函数
主要作用在于对象销毁前系统自动调用,执行一些内存清理工作。构造函数和析构函数如下所示:
#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; }6.深拷贝和浅拷贝
- 浅拷贝:一定切记是共享内存空间,对于像
int,float,char这样的基本数据类型,浅拷贝会创建一个新的、独立的副本;对于像指针、引用这样的复杂类型,浅拷贝只会复制指针本身,而不会复制指针指向的内容。结果就是,新旧两个对象会共享同一块内存资源。 - 深拷贝:不共享内存空间,对于基本数据类型,和浅拷贝一样,创建新副本;对于指针、引用等复杂类型,深拷贝会不仅复制指针,还会为指针指向的内容分配新的内存,并将内容也复制过去。最终,新旧对象拥有各自独立的资源,互不干扰。
- 浅拷贝带来的问题:内存的重复释放问题,解决办法,采用深拷贝。
7.静态成员
静态成员变量:
- 类内声明,类外初始化;
- 它属于整个类,而不属于某个具体的对象;
- 作用范围有限,但生命周期贯穿整个程序;
- 相当于局部全局变量;
静态成员函数:
- 可通过命名空间直接访问,不需要通过创建对象去访问;
- 它属于整个类,而不属于某个具体的对象;
- 只能访问静态成员,不能访问非静态成员;
8.非常关键内容
在c++中成员变量和成员函数是分开存储,只有非静态成员变量才会占据类对象的内存空间,静态成员变量和函数与常规成员函数不会占用类对象的内存空间。
9.this指针(本质相当于指向类对象的指针常量)
写在前面:常见用法解决命名冲突、返回对象本身实现链式调用,本质是让成员函数 “知道操作哪个对象”。
- 定义:
this指针是 C++ 编译器自动为每个非静态成员函数添加的一个隐藏的、常量指针(const指针),它指向调用该成员函数的那个对象本身。简单说:谁调用函数,this就指向谁。 - this指针的用途:
当形参和成员变量同名时,可用this指针来区分,如代码1; 在类的非静态成员函数中返回对象本身,可使用return *this,如代码2;
在成员函数中访问当前对象的地址 / 本身,如代码3;
this的类型是类名* 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; }10.友元
总结:只要看见friend关键字,那么这个函数就可以访问引入它的类的私有成员。
2.2继承
1.继承方式
public、protect、private(上面已经介绍过)
2.继承中的构造和析构
子类继承父类时,继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
3.菱形继承
何为菱形继承,可以简单理解为,两个父类继承同一个爷爷,孙子类又继承两个父类,继承父类两份相同的数据,会导致资源浪费以及毫无意义,利用虚继承可以解决菱形继承问题,当两个派生类继承同一个基类时,采用虚继承。
2.3多态
1.分类
静态多态:
定义:编译器在编译阶段就确定要调用哪个函数,也叫 “早绑定”。
实现方式:函数重载、运算符重载,代码1;
动态多态:
定义:编译器在编译阶段无法确定调用哪个函数,直到程序运行时才确定,也叫 “晚绑定”。
实现方式:虚函数(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; } 2.虚函数和纯虚函数
虚函数:普通虚函数是用 virtual 关键字修饰的、有函数体的成员函数,目的是允许子类重写(override),从而实现动态多态。
纯虚函数:纯虚函数是用 virtual 修饰、没有函数体,且以 = 0 结尾的成员函数,目的是强制子类必须重写该函数,同时将包含它的类变为抽象类,抽象类不能实例化对象(无法创建 Animal a; 这样的对象)。
三.STL
STL(Standard Template Library,标准模板库);从广义上分为容器(container) 算法(algorithm) 迭代器(iterator),容器和算法之间通过迭代器进行无缝连接。
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
2. 算法:各种常用的算法,如sort、find、copy、for_each等
3. 迭代器:扮演了容器与算法之间的胶合剂。
4. 仿函数:行为类似函数,可作为算法的某种策略。
5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
6. 空间配置器:负责空间的配置与管理。