MSVC 参考文档 https://learn.microsoft.com/zh-cn/cpp/cpp/?view=msvc-170
C++ 面向对象
指针
new;
delete;
// single element
int p = new int(10);
cout << *p << endl;
delete p;
cout << *p << endl; // RE illegal mem access
// array
int *arr = new int[10];
// int[10] a = new int[10]; // 复习 Java 语法
引用
int& test02() {
static int a = 10;
return a;
}
这个函数的返回值可以用 int
变量来接受。不过要注意的是,这样会发生值拷贝,返回的 int
引用会被赋值给新的 int
变量,实际接收的是 a
的拷贝值,而不是 a
的引用。
例如:
int b = test02(); // 合法,b 得到的是 a 的拷贝
如果你想要接收引用本身,应该用 int&
来接收:
int& b = test02(); // b 是对 a 的引用
在这种情况下,b
实际上是 a
的引用,任何对 b
的修改都会直接影响 a
。
struct 和 class 区别
struct
默认公有,而 class
是私有。
权限
public
: 公有
private
: 私有
protected
: 继承类可访问
setter / getter
命名规范
函数名驼峰,类名帕斯卡(驼峰 + 首字母大写)
对象特性
对象的初始化和和清理非常重要,由此产生了构造函数和析构函数的概念。
构造函数
ClassName() {}
可以重载,创建对象后调用
构造函数的分类和调用
按照参数可分为无参数和有参数
class Person {
public:
int age;
Person() {
cout << "Person 的无参构造函数调用" << endl;
}
Person(int a) {
cout << "Person 的有参构造函数调用" << endl;
}
Person(const Person &p) { // 注意这里要加 const
cout << "Person 的拷贝构造函数调用" << endl;
}
};
void test01() {
// 1 括号法
Person p1;
Person p2(10);
Person p3(p2);
// 显式法
Person p1;
Person p2 = Person(10);
Person p3 = Person(p2);
Person(10); // anonymous
// 隐式转换法
Person p4 = 10; // C++ 11 以上
}
注意!
- 调用默认构造函数的时候,不要加
()
,否则编译器会将该语句误认为是声明函数。 - 不要利用拷贝构造函数初始化匿名对象,这样会被误认为是函数对象的声明。
拷贝构造函数调用的时机
使用一个创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
值传递方式返回局部对象
拓展:深拷贝浅拷贝
构造函数的调用规则
C++ 编译器会给每个类创建至少 3 个函数:
- 默认构造函数(空参数),函数体为空;
- 默认拷贝构造函数 值拷贝(shallow copy);
- 默认析构函数(空参数),函数体为空;
**注意!**自己编写构造函数之后,编译器将不再生成构造函数。具体来说,如果自己编写了拷贝构造函数,编译器既不会生产空参数的构造函数,也不会保留原来的拷贝构造函数。
深拷贝浅拷贝
浅拷贝:只拷贝属性的指针或引用
深拷贝:new
一个新的对象,并让属性指向这个对象
如果属性有在堆区开辟的,一定要自己实现析构函数
这里和 Java 相比麻烦多了!!!Java 有内存垃圾回收机制,不用自己实现析构函数。如果深拷贝开销过大,且浅拷贝所带来的共享对象的副作用可以忽略的话,可以采用浅拷贝,这里和 C++ 的编程习惯是不同的。
列表初始化
class Person {
public:
int m_A;
int m_B;
int m_C;
Person(int a, int b, int c) : m_A(a), m_B(b), m_C(C) {} // 简化了初始化赋值操作,函数体可以进行其他初始化操作
};
析构函数
~className() {}
不可重载,销毁对象前调用
系统提供的默认构造和析构函数是空实现。
类和对象
- 封装
- 继承
- 多态
类对象作为成员
class Phone {
public:
Person(string pName) : m_PName(pName) {
}
string m_PName;
};
class Person {
public:
Person(string name, string pName) : m_Name(name), m_Phone(pName) {
}
string m_Name;
Phone m_Phone;
};
这里的 m_Phone(pNname)
相当于 Phone m_Phone = pName
,隐式转换法
先构造成员,再构造主类。析构的顺序和构造相反。
静态成员
class Person {
public:
static int m_A;
};
int Person::m_A = 100; // static
-
静态成员变量
- 类内声明,类外初始化.
静态成员函数
- 静态成员函数只能访问静态成员变量
成员变量和成员函数分开存储
class Person {
};
只有非静态成员变量属于该对象所占空间的大小
空对象占用 1 个字节空间,以区分不同的对象。
this 指针的用途
this 指针指向的是被调用的成员函数所指向的对象
复习:用值的方式返回,就会产生新的对象,而使用引用,返回的是对象的本体。
这个知识点经常出现在链式编程思想中。
空指针访问成员函数
C++ 允许空指针访问成员函数,但是遇到 this
指针的访问就会崩溃,因为当前指针并没有指向一个合法对象实例的地址,从而无法获取成员变量的信息。
const
修改成员函数
class Person {
public:
void showPerson() const {
// this->m_A = 100;
}
int m_A;
mutable int m_B; // 这样在常函数中就可以修改了。回忆下 Java 中的 Mutable 和 非 Mutable 对象
};
常函数只能调用常函数(避免副作用)
封装
属性和类作为一个整体
const double PI = 3.14
class Circle {
public:
int m_r;
double calculateZC() {
return 2 * PI * m_r;
}
}; // C++ 结构体和类结束的地方有分号
类中的属性和成员函数均称为属性
属性一般设置为私有
class person {
public:
string m_Name;
};
用 Getter Setter
来修改和展示变量的值。
- public 公有权限
- protected 保护权限
- private 私有
struct
默认公有,class
默认私有。
struct
在算法类竞赛等时间紧迫的场景下较为常用。
将属性设置为私有
- 可以自己控制读写权限
- 对于写可以检测数据有效性
class person {
private:
string m_Name;
int m_Age;
string Idol;
public:
string getName() {
}
int getAge() {
return m_Age;
}
void setAge(int age) {
if (age < 0 || age > 150) {
cout << "年龄设置失败" << endl;
return;
}
m_Age = age;
}
string setIdol(string Idol) {
this->Idol = Idol;
}
};
综合案例
立方体的设计
设计立方体类
设计属性
设计函数判断两个立方体是否相等
// 利用全局函数
// 利用成员函数
// 成员函数更好
继承
共有继承和私有继承
- 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。 - 私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。 - 保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。
多态
多态按字面的意思就是多种形态。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
在 C++ 中,多态(Polymorphism)是面向对象编程的重要特性之一。
C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。
多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。
以下是多态的几个关键点:
虚函数(Virtual Functions):
在基类中声明一个函数为虚函数,使用关键字 virtual。
派生类可以重写(override)这个虚函数。
调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。
动态绑定(Dynamic Binding):
也称为晚期绑定(Late Binding),在运行时确定函数调用的具体实现。
需要使用指向基类的指针或引用来调用虚函数,编译器在运行时根据对象的实际类型来决定调用哪个函数。
纯虚函数(Pure Virtual Functions):
一个包含纯虚函数的类被称为抽象类(Abstract Class),它不能被直接实例化。
纯虚函数没有函数体,声明时使用= 0。
它强制派生类提供具体的实现。
多态的实现机制:
虚函数表(V-Table):C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。
虚函数指针(V-Ptr):对象中包含一个指向该类虚函数表的指针。
使用多态的优势:
代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
解耦:多态允许程序设计更加模块化,降低类之间的耦合度。
注意事项:
只有通过基类的指针或引用调用虚函数时,才会发生多态。
如果直接使用派生类的对象调用函数,那么调用的是派生类中的版本,而不是基类中的版本。
多态性需要运行时类型信息(RTTI),这可能会增加程序的开销。
和 Java 多态的区别:
C++ | Java |
---|---|
虚函数 | 普通函数 |
纯虚函数 | 抽象函数 |
抽象类 | 抽象类 |
虚基类 | 接口 |
- 抽象类是不能实例化的类,其他的要求和普通类一致。
- 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
- 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。
虚函数相关
动态多态
class Animal {
public:
virtual void speak() {
cout << "动物在说话" << endl;
}
};
class Cat : public Animal {
public:
virtual void speak() {
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal {
virtual void speak() {
cout << "小狗在说话" <<< endl;
}
};
// 静态多态(不加 virtual)地址早绑定,在编译阶段确定
void doSpeak(Animal &animal) {
Animal.speak();
}
//
虚函数在父类中必须加,子类中选加
动态多态的条件:
- 继承关系
- 子类重写父类函数
内部机制详解
Animal 类内部结构
vfptr -> vftable(表内记录虚函数的地址)
&Animal::speak
Cat 类内部结构
vfptr -> vftable
&Animal::speak 被 &Cat::speak 替换
这些信息都驻留在内存
当父类指针或者引用指向子类对象的时候,发生多态。
vs 开发人员命令提示符
cl /d1 reportSingleClassLayout[ClassName] [Programname].cpp
开闭原则
对扩展进行开发,对修改进行关闭
使用多态好处:
- 组织结构清晰
- 可读性强
- 对前后期的扩展和维护十分方便
纯虚函数和抽象类
virtual 返回值类型 函数名 (参数列表) = 0;
当一个类中有了纯虚函数,这个类称为抽象类
子函数如果不重写纯虚函数,则也是抽象类
虚析构和纯虚析构函数
纯虚析构 =0
声明之后还要实现
解决通过父类指针释放子类对象的问题
只要有纯虚析构就是抽象类了
virtual ~Animal() = 0; // 纯虚析构
/* 类外加上实现 */ Animal::~Animal() {}
virtual Animal() {} // 虚析构
Case Study
#include<iostream>
using namespace std;
class CPU {
public:
virtual void calculate() = 0;
};
class GPU {
public:
virtual void display() = 0;
};
class Memory {
public:
virtual void ldst() = 0;
};
class computer {
public:
computer(CPU cpu, GPU gpu, Memory memory) {
m_cpu = cpu, m_gpu = gpu, m_memory = memory;
}
void work() {
m_cpu->calculate();
m_memory->display();
m_gpu->ldst();
}
~computer() {
if (m_cpu != nullptr) {
delete(m_cpu);
m_cpu = nullptr;
}
if (m_gpu != nullptr) {
delete(m_gpu);
m_gpu = nullptr;
}
if (m_memory != nullptr) {
delete(m_memory);
m_memory = nullptr;
}
}
private:
CPU m_cpu;
GPU m_gpu;
Memory m_memory;
};
class AMDCPU : public CPU {
public:
void calculate() {
cout << "AMD CPU 开始工作了" << endl;
}
};
class AMDGPU : public GPU {
public:
void display() {
cout << "AMD GPU 开始工作了" << endl;
}
};
class AMDMemeory : public Memory {
void ldst() {
cout << "AMD Memory 开始工作了" << endl;
}
};
class IntelCPU : public CPU {
public:
void calculate() {
cout << "Intel CPU 开始工作了" << endl;
}
};
class IntelGPU : public GPU {
public:
void display() {
cout << "Intel GPU 开始工作了" << endl;
}
};
class IntelMemeory : public Memory {
void ldst() {
cout << "Intel Memory 开始工作了" << endl;
}
};
// provide a deconstrutive function
int main() {
// the first cpu
CPU *intelCPU = new IntelCPU(); // 也可以不加括号
GPU *intelGPU = new IntelGPU();
Memory *intelMem = new IntelMemory();
Computer *computer1 = new Computer(intelCPU, intelGPU, intelMem);
computer1->work();
delete(computer1);
return 0;
// the second cpu
}
VSCode C++
.vscode 中的文件
tasks.json
launch.json
c_cpp_properties.json
C++ 文件 IO
回顾:C 语言文件的操作
文件操作
<fstream>
- 文本文件
- 二进制文件
文件操作的类型
ofstream
ifstream
fstream