#include 后面使用 尖括号 <> 和 双引号 "" 的主要区别
| 写法 | 搜索顺序 | 用途 |
|---|---|---|
#include <file> |
仅系统/编译器指定的 include 目录 | 标准库、第三方库 |
#include "file" |
1. 当前目录 → 2. 系统目录(退化行为) | 项目自定义头文件 |
函数的分文件编写
作用:让代码结构更加清晰
函数份文件编写一般有4个步骤
- 创建后缀名.h的头文件
- 创建后缀名cpp的源文件
- 在头文件中写函数的声明
- 在源文件中写函数的定义
空指针和野指针
空指针
空指针:指针变量指向内存中编号为0的空间
用途 :初始化指针变量
注意:空指针指向的内存是不可以访问的
指针常量和常量指针
| 类型 | 声明形式 | 能否改指向? | 能否改值? | 常见用途 |
|---|---|---|---|---|
| 常量指针 | const int* p |
✅ 可以 | ❌ 不可以 | 保护数据不被意外修改 |
| 指针常量 | int* const p |
❌ 不可以 | ✅ 可以 | 固定指向某个地址(如硬件寄存器) |
| 两者皆常量 | const int* const p |
❌ | ❌ | 完全只读引用 |
指针函数 和函数指针
| 名称 | 是什么? | 声明示例 | 关键特征 |
|---|---|---|---|
| 指针函数 | 返回指针的函数 | int* foo(); |
函数,返回值是指针 |
| 函数指针 | 指向函数的指针 | int (*p)(int); |
变量,存储函数地址 |
结构体指针
作用:通过指针访问结构体中的成员
- 利用**操作符
->**可以通过结构体指针访问结构体属性
内存分区模型
C++程序在执行时,将内存大方向分为4个区域
代码区 :存放函数体的二进制代码,由操作系统进行管理的
全局区: 存放全局变量和静态变量以及常量
栈区 :由编译器自动分配释放,存放函数的参数值,局部变量等
堆区: 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收内存四区意义:
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程
C++ 和 Java 创建对象的区别
c++的数组和Java数组的不同
值传递 、指针传递(地址传递) 和 引用传递的区别
值传递 、指针传递(地址传递) 和 引用传递的区别
cpp//1、值传递 void mySwop01(int a, int b) { int temp = a; a = b; b = temp; } //2、地址传递 void mySwop02(int* a, int* b) { int temp = *a; *a = *b; *b = temp; } //3、引用传递 void mySwop03(int& a, int& b) { int temp = a; a = b; b = temp; }
特性 值传递 地址传递(指针) 引用传递 能否修改原变量 ❌ 不能 ✅ 能 ✅ 能 传参方式 func(x, y)func(&x, &y)func(x, y)函数内操作对象 副本 通过 *p解引用操作原变量直接操作原变量(别名) 安全性 最安全(隔离) 可能空指针崩溃 安全(引用不能为空) 性能 小类型快,大对象慢(拷贝) 快(只传地址) 快(无拷贝,无解引用开销) C 语言支持 ✅ ✅ ❌(C++ 特有)
- 想修改原变量? → 优先用 引用传递 (
int&)。- 不想修改,且对象很大? → 用
const T&避免拷贝。- 需要表示"可选"参数(可能为空)? → 用指针(如
int* p)。- 只是读小数据(如
int)? → 值传递即可,简单清晰。
引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
cpp#include<iostream> using namespace std; //引用做函数的返回值 //1、不要返回局部变量的引用 int& test01() //相当于现在test01()函数是 a的别名了,a可以做左值,函数当然也可以 { int a = 10; //局部变量存放在四区中的 栈区 return a; } //2、函数的调用可以作为左值 int& test02() { static int a = 10; //静态变量,存放在全局区,全局区上的数据在程序结束后释放 return a; } int main() { int& ref = test01(); cout << "ref=" << ref << endl; //正确,编译器做了保留 cout << "ref=" << ref << endl; //错误,乱码,因为a的内存已经释放 int& ref2 = test02(); cout << "ref2=" << ref2 << endl; cout << "ref2=" << ref2 << endl; //ref2是a的别名 test02() = 1000; //如果函数的返回值是引用,这个函数调用可以作为左值 cout << "ref2=" << ref2 << endl; cout << "ref2=" << ref2 << endl; system("pause"); return 0; }
函数占位参数
- C++中函数的形参列表中可以有占位参数,用来做占位,调用函数时必须填补该位置。
要用于以下场景:
- 保持函数签名兼容(如重载、模板特化)
- 满足接口要求但不需要实际使用该参数
- 为未来扩展预留参数位置
- 与 C 风格回调函数兼容
cppvoid func(int, double, char); // 声明:三个占位参数 void func(int, double x, char) { // 定义:只用了 x cout << "x = " << x << endl; } int main() { func(10, 3.14, 'A'); // 调用时必须传三个实参! }
函数重载--引用 const
函数重载注意事项
- 引用作为重载条件
- 函数重载碰到函数默认参数
cpp#include<iostream> using namespace std; //函数重载的注意事项 //1、引用作为重载的条件 void func(int& a) { cout << "func(int& a)调用" << endl; } void func(const int& a) //类型不同 { cout << "func(const int& a)调用" << endl; } int main() { int a = 10; func(a); //调用没有const的函数 func(10); //调用有const的函数 system("pause"); return 0; }
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
静态成员(类内声明,类外初始化)
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量:所有对象共享同一份数据,在编译阶段分配内存,类内声明,类外初始化(类内初始化会报错)
- 静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量
成员变量和成员函数分开存储
cpp
#include<iostream>
using namespace std;
//成员变量 和 成员函数 分开存储的
class base {
};
void test01() {
base p;
//空对象占用内存空间为:1
//C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置
//每个空对象也应该有一个独一无二的内存地址
cout << "sizeof(p):" << sizeof(p) << endl; //Person类里面无内容:1字节
}
class Person {
int m_A; //非静态成员变量 属于类的对象上
static int m_B; //静态成员变量 不属于类对象上
void func() {} //非静态成员函数 不属于类对象上
static void func2() {} //静态成员函数 不属于类对象上
};
int Person::m_B = 0;
void test02() {
Person p;
cout << "sizeof(p):" << sizeof(p) << endl; //Person类里面有int m_A:4字节
}
int main() {
test01();
test02();
system("pause");
return 0;
}
this指针概念
在C++中成员变量和成员函数是分开存储的
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用
return *this空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
cpp#include<iostream> using namespace std; //空指针调用成员函数 class Person { public: void showClassName() { cout << "this is Person class" << endl; } void showPersonAge() { //报错, 原因是传入的指针是为NULL if (this == NULL) { return; } cout << "age=" << m_Age << endl; } int m_Age; }; void test01() { Person* p = NULL; p->showClassName(); p->showPersonAge(); } int main() { test01(); system("pause"); return 0; }
const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
运算符重载++
cpp
class MyClass {
public:
// 前置 ++:无参数
MyClass& operator++(); // 返回引用
// 后置 ++:带一个 int 参数(仅用于区分,不使用)
MyClass operator++(int); // 返回值(副本)
};
// 前置
Counter& operator++() {
++value;
return *this;
}
// 后置:复用前置
Counter operator++(int) {
Counter old = *this; // 拷贝当前状态
++(*this); // 调用前置++
return old; // 返回旧值
}
左移运算符重载<<
作用:可以输出自定义数据类型
cpp
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {}
// 声明友元函数:允许访问 private 成员
friend std::ostream& operator<<(std::ostream& os, const Person& p);
};
// 定义友元函数(非成员)
std::ostream& operator<<(std::ostream& os, const Person& p) {
os << "Person(name: " << p.name << ", age: " << p.age << ")";
return os; // 返回 os 以支持链式调用(如 cout << a << b;)
}
int main() {
Person p("Alice", 30);
std::cout << p << std::endl; // ✅ 输出:Person(name: Alice, age: 30)
return 0;
}
继承中构造和析构顺序
在 C++ 的继承体系 中,构造函数和析构函数的调用顺序是有严格规定的
构造顺序:从基类到派生类(先父后子)
先构造基类 → 再构造派生类
📌 规则细节:
- 首先调用最顶层基类的构造函数。
- 然后逐层向下,依次调用中间基类(如果有多重继承,则按声明顺序)。
- 最后调用派生类自身的构造函数。
- 如果有成员对象 ,它们的构造发生在该类构造函数体执行之前(按声明顺序)。
析构顺序:从派生类到基类(先子后父)
先析构派生类 → 再析构基类
📌 规则细节:
- 先调用派生类的析构函数。
- 然后逐层向上,依次调用基类的析构函数(顺序与构造相反)。
- 成员对象的析构发生在该类析构函数体执行之后 ,按声明的逆序。
带成员对象的完整示例
cpp#include <iostream> class Member { public: Member(const char* name) { std::cout << name << " 成员构造\n"; } ~Member() { std::cout << "成员析构\n"; } }; class Base { Member m1{"Base.m1"}; public: Base() { std::cout << "Base 构造函数体\n"; } ~Base() { std::cout << "Base 析构函数体\n"; } }; class Derived : public Base { Member m2{"Derived.m2"}; public: Derived() { std::cout << "Derived 构造函数体\n"; } ~Derived() { std::cout << "Derived 析构函数体\n"; } }; int main() { Derived d; }
cppBase.m1 成员构造 Base 构造函数体 Derived.m2 成员构造 Derived 构造函数体 Derived 析构函数体 成员析构 // m2 Base 析构函数体 成员析构 // m1
继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
- 子类对象可以直接访问到子类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
菱形继承-----虚继承
菱形继承(Diamond Inheritance) 是 C++ 多重继承中的一种特殊结构,指一个派生类通过两条不同的继承路径继承了同一个基类,形成类似"菱形"的继承图。它会引发 数据冗余 和 二义性 问题。
解决方案:虚继承(Virtual Inheritance)
使用
virtual关键字 让B和C共享同一个A的实例。
cppclass A { public: int x = 10; void show() { std::cout << "A::show, x=" << x << "\n"; } }; class B : virtual public A {}; // 虚继承 class C : virtual public A {}; // 虚继承 class D : public B, public C {}; // D 只有一个 A 的副本 int main() { D d; d.x = 42; // ✅ 无二义性!只有一个 x d.show(); // ✅ 正常调用 std::cout << d.x << "\n"; // 输出: 42 }虚继承确保:无论通过多少条路径继承,共同基类
A在D中只有唯一一份实例。
c++的多态
序号 名称 实际归属 说明 1 重载多态(Overloading Polymorphism) 编译时 函数/运算符重载 2 强制多态(Coercion Polymorphism) 编译时 隐式类型转换(如 int→double)3 参数多态(Parametric Polymorphism) 编译时 模板(泛型) 4 包含多态(Inclusion Polymorphism) 运行时 虚函数 + 继承(子类型多态)
- 编译时多态(静态多态)
- 在编译阶段就确定调用哪个函数。
- 实现方式:
- 函数重载(Function Overloading)
- 运算符重载(Operator Overloading)
- 模板(Templates) → 泛型编程的核心
📌 特点:无运行时开销,效率高。
- 运行时多态(动态多态)
- 在程序运行时根据对象实际类型决定调用哪个函数。
- 实现方式:
- 虚函数(virtual functions) + 继承
- 通过基类指针/引用调用派生类重写的函数
📌 特点:灵活,但有虚表(vtable)和间接调用的开销。
c++中只有虚函数才能实现动态多态,否则成员函数调用由指针引用的静态类型决定
cpp#include <iostream> class Base { public: void show() { std::cout << "Base::show\n"; } // ❌ 非虚函数 }; class Derived : public Base { public: void show() { std::cout << "Derived::show\n"; } }; int main() { Derived d; Base* p = &d; // 静态类型是 Base*,动态类型是 Derived* p->show(); // 输出: Base::show }
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0
;当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
通常情况下,纯虚函数没有函数体(即不提供实现),
但 C++ 允许为纯虚函数提供函数体!也就是说:
🔹 可以没有函数体 (最常见)
🔹 也可以有函数体(合法且有用)
纯虚函数可以有函数体吗?✅ 可以!
虽然声明为
= 0,但你仍然可以在类外(或 C++11 起在类内)提供定义:
cpp#include <iostream> class Base { public: virtual void func() = 0; // 声明为纯虚 }; // 提供函数体(类外定义) void Base::func() { std::cout << "Base::func() called\n"; } class Derived : public Base { public: void func() override { Base::func(); // 显式调用基类的纯虚函数实现 std::cout << "Derived::func()\n"; } }; int main() { Derived d; d.func(); }
cppBase::func() called Derived::func()
虚析构和纯虚析构
在 C++ 多态中,虚析构函数 和 纯虚析构函数 是解决通过基类指针安全释放派生类对象 的关键机制。它们的核心目标是:确保派生类的析构函数被正确调用,避免内存泄漏。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
cpp虚析构语法: virtual ~类名(){} 纯虚析构语法: virtual ~类名() = 0; 类名::~类名(){}问题背景:为什么需要虚析构?
cppclass Base { public: ~Base() { cout << "Base 析构\n"; } }; class Derived : public Base { private: int* data; public: Derived() { data = new int(42); } ~Derived() { delete data; cout << "Derived 析构\n"; } }; int main() { Base* p = new Derived(); delete p; // ❌ 只调用 Base::~Base()! }后果:
Derived::~Derived()未被调用data指向的堆内存未释放 → 内存泄漏原因:非虚析构函数是静态绑定 ,编译器只看指针类型(
Base*),不关心实际对象类型。解决方案 1:虚析构函数
cppclass Base { public: virtual ~Base() { // 加 virtual 关键字 cout << "Base 虚析构\n"; } }; int main() { Base* p = new Derived(); delete p; // 输出: // Derived 析构 // Base 虚析构 }结果
- 通过基类指针
delete时:
- 先调用 派生类析构函数
- 再调用 基类析构函数
- 确保资源完整释放
为什么纯虚析构必须有函数体?
- 析构函数的特殊性 :
即使声明为纯虚,派生类析构时仍会自动调用基类析构函数。 - 如果没有定义,链接器找不到
Base::~Base()的实现 → 链接错误。
C++11 及以后:允许类内定义
cpp
class Base {
public:
virtual ~Base() = 0 {
// 函数体直接写在这里!
std::cout << "Pure virtual destructor\n";
}
};