0%
前言
C++类和对象学习笔记
定义类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class 类名 {
权限修饰符: 数据类型 属性名;
权限修饰符: 返回值类型 函数名(参数列表) { return 返回值; } };
|
创建对象
获取对象的属性
为对象的属性赋值
调用对象的行为
权限修饰符
- 权限修饰符可以加在属性前或方法前
- 权限修饰符定义一次后,权限修饰符之后的所有属性和函数都被修饰
关键字 |
权限 |
类内是否可以访问成员 |
类外是否可以访问成员变量 |
是否可以被继承 |
public |
公共权限 |
✓ |
✓ |
✓ |
protected |
保护权限 |
✓ |
× |
✓ |
private |
私有权限 |
✓ |
× |
× |
1 2 3 4 5 6 7 8 9 10 11 12
| class 类名 { public: 数据类型 公共权限的变量名; 数据类型 公共权限的变量名; protected: 数据类型 保护权限的变量名; 数据类型 保护权限的变量名; private: 数据类型 私有权限的变量名; 数据类型 私有权限的变量名; };
|
struct和class
- struct和class的默认访问权限不同
- struct的默认访问权限是public公共权限
- class的默认访问权限是private私有权限
封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class 类名 { private: int global_id; public: int getId() { return global_id; } void setName(int id) { global_id = id; } };
|
对象的初始化和清理
- 在类中定义构造函数用来对象的初始化
- 在类中定义析构函数用来对象的清理
- 如果定义类的时候没有定义构造函数和析构函数,那么编译器会提供空的构造方法和析构方法
构造函数
定义构造函数
- 在创建对象时自动执行构造方法
- 构造函数没有返回值也不写void
- 构造函数的方法名与类名相同
- 构造函数可以有参数列表,所以可以有重载方法
定义无参构造
1 2 3 4 5 6 7 8
| class 类名 { public: 类名() { ... } };
|
定义有参构造
1 2 3 4 5 6 7 8
| class 类名 { public: 类名(参数列表) { ... } };
|
定义拷贝构造
1 2 3 4 5 6 7 8 9 10
| class 类名 { 数据类型 属性名;
public: 类名(const 类名 & 对象名) { 属性名 = 对象名.属性名; } };
|
调用构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 类名 对象名;
类名 对象名(参数列表);
类名 对象名 = 类名(参数列表);
类名 对象名 = 参数;
类名(参数列表);
|
析构函数
- 在对象销毁前自动执行析构方法
- 析构函数没有返回值也不写void
- 析构函数的方法名在类名前加
~
- 析构函数没有参数列表,所以没有重载方法
1 2 3 4 5 6 7 8
| class 类名 { public: ~类名() { ... } };
|
拷贝构造函数的调用时机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Persion { public: Persion(int i) { ... } Persion(const 类名 & 对象名) { ... } };
Persion method(Persion p) { Persion p1(10); return p1; }
void main() { Persion p1(10); Persion p2(p1); method(p1); }
|
默认构造函数
- 只要定义一个类,就默认存在的函数:
- 默认构造函数,无参且函数体为空
- 默认析构函数,函数体为空
- 默认拷贝构造函数,函数体中对属性值进行拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13
| class 类名 { 类名() {} 类名(类名 对象名) { ... } ~类名() {} };
|
- 如果自定义了有参构造函数,C++不会提供无参构造函数,但是会提供拷贝构造函数
- 如果自定义了拷贝构造函数,C++不会提供无参构造函数和其他构造函数
深拷贝与浅拷贝
- 如果拷贝的属性没有指针,那么使用浅拷贝即可
- 如果拷贝的属性有指针,那么需要手动定义深拷贝来使用
浅拷贝
- 编译器默认提供的拷贝构造函数是浅拷贝,浅拷贝的实质是把成员属性的值拷贝一份
- 当拷贝的成员属性是指针的时候,浅拷贝只会拷贝一份指针数据,不会在堆内存中创建新的空间,所带来的问题是,释放指针时会出现堆区内存重复释放的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Persion { int * id;
Persion(const Persion & p) { id = p.id; } ~Persion() { if (id != NULL) { delete id; } } };
|
深拷贝
- 深拷贝需要自定义一个新的拷贝函数,深拷贝会在堆区开辟一块新的空间
- 当拷贝的成员属性是指针的时候,深拷贝会堆区开辟一块新的空间,所以存放的是一个新的指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Persion { int * id;
Persion(const Persion & p) { id = new int(*p.id); } ~Persion() { if (id != NULL) { delete id; } } };
|
初始化列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Persion { int global_id int global_num Persion(int id, int num) { global_id = id; global_num = num; } Persion(int id, int num): global_id(id), global_num(num) { } };
|
类对象作为类成员
1 2 3 4 5 6 7 8 9
| class Son {
};
class Father { Son son; };
|
静态成员
- 在成员属性和成员函数前添加
static
关键字,就构成了静态成员
- 静态成员变量也受访问权限的制约
静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
1 2 3 4 5 6
| class 类名 { static 数据类型 属性名; };
数据类型 Persion::属性名 = 值;
|
静态成员变量的访问
通过对象访问
通过类名访问
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量,非静态成员变量无法访问
1 2 3 4 5 6 7
| class 类名 { static 返回值类型 函数名() { ... } };
|
静态成员函数的访问
通过对象访问
通过类名访问
this指针
- this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义可以直接使用
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态函数中返回对象本身,可用
*this
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Persion { int id; Persion(int id) { this->id = id; } Persion& method() { return *this; } };
|
常函数与常对象
常函数
- 声明函数时添加
const
关键字
- 常函数内不可修改成员属性(相当于this指针改为了常量)
- 常函数内只能调用常函数
1 2 3 4
| 返回值类型 函数名 const { retuen 返回值; }
|
常对象
- 声明对象时添加
const
关键字
- 常对象只能调用常函数
常函数和常对象都能修改的成员属性
- 如果需要修改成员属性,可以在成员属性声明时添加
mutable
关键字,使得常含数内的成员属性可以被修改
类外定义成员函数
定义普通函数
1 2 3 4 5 6 7 8 9
| class Persion { void method(); };
void Persion::method() { ... }
|
定义构造函数
1 2 3 4 5 6 7 8 9
| class Persion { Persion(); };
Persion::Persion() { ... }
|
友元
- 友元的目的是让一个函数或者类访问另一个类的私有成员
成员函数做友元
1 2 3 4 5 6 7 8 9 10 11 12
| class 类名 { friend 返回值类型 函数名(类名 &对象名);
private: int id; };
返回值类型 函数名(类名 &对象名) { 对象名.id; }
|
类做友元
1 2 3 4 5 6 7 8 9 10 11 12
| class 类名 { friend class Friend;
private: int id; };
class Friend { ... };
|
成员函数做友元
- 通过
friend
关键字在类中定义其他类中的函数作为友元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class 类名 { friend class Friend::method();
private: int id; };
class Friend { void method() { ... } };
|
运算符重载
算数运算符重载
- 此处以加法的运算符重载为例
- 为了能实现连续运算,返回值类型为类对象
+
:指定被重载的运算符,这里重载的运算符是加法
通过成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Persion { private: int id; public: Persion() {} Persion(int id) { this->id = id; } Persion operator+(Persion &persion) { Persion temp; temp.id = this->id + persion.id; return temp; } };
int main() { Persion p1(1); Persion p2(1); Persion p3 = p1 + p2; return 0; }
|
通过全局函数
- 直接在运算符重载函数的参数列表制定两个类型的参数,可以是不同类型的数据(例如自定义数据类型与int数据类型)
- 内置的数据类型是不可以改变的(例如两个int类型数据相加)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Persion { friend Persion operator+(Persion &persion1, Persion &persion2); private: int id; public: Persion() {} Persion(int id) { this->id = id; } };
Persion operator+(Persion &persion1, Persion &persion2) { Persion temp; temp.id = persion1.id + persion2.id; return temp; }
int main() { Persion p1(1); Persion p2(1); Persion p3 = p1 + p2; return 0; }
|
左移运算符重载
- 可以实现输出任意自定义数据类型的数据
- 左移运算符重载的函数返回值为空,函数体内拼接需要输出的内容
通过全局函数
- 因为封装的时候通常将成员变量私有化,所以为了能让左移运算符重载函数可以访问私有成员变量,通常在类内定义友元
- 为了能实现连续左移,返回值类型为ostream类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class Persion { friend ostream & operator<<(ostream &cout, Persion &persion); private: int id; public: Persion(int id) { this->id = id; } };
ostream& operator<<(ostream &cout, Persion &persion) { cout << "id=" << persion.id; return cout; }
int main() { Persion p(1); cout << p << endl; return 0; }
|
递增运算符重载
- 前置递增
- 为了能实现递增的嵌套,返回值类型需要设置为引用类型
- 后置递增
- 返回值类型需要设置为值
- 为了能实现递增的嵌套,后置递增运算符重载函数的参数需要用int作为占位参数,且只能是int,用于区分前置运算符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| class Persion { private: int id; public: Persion(int id) { this->id = id; } Persion& operator++() { this->id++; return *this; } Persion operator++(int) { Persion temp = *this; this->id++; return temp; } };
int main() { Persion p(1); p++; ++p; return 0; }
|
赋值运算符重载
- 默认的赋值运算符是浅拷贝数据,重载赋值运算符
- 为了能实现连等,返回值类型为类对象的引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| class Persion { private: int *id; public: Persion(int id) { this->id = new int(id); } Persion& operator=(Persion &persion) { if (this->id != NULL) { delete this->id; this->id = NULL; } this->id = new int(*persion.id); return *this; } };
int main() { Persion p1(1); Persion p2 = p1; return 0; }
|
关系运算符重载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Persion { private: int id; public: Persion(int id) { this->id = id; } bool operator==(Persion &persion) { return this->id == persion.id ? true : false; } };
int main() { Persion p1(1); Persion p2(1); p1==p2; return 0; }
|
函数调用运算符重载
- 函数调用运算符(
()
)也可以重载,由于重载后使用的方式非常像函数的调用,因此称为仿函数,仿函数没有固定写法,非常灵活
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Persion { public: Persion() {} 返回值类型 operator()(参数列表) { return 返回值; } };
int main() { Persion persion; persion(参数); Persion()(参数); return 0; }
|
继承
- 将重复的代码定义为父类(基类),子类(派生类)继承父类,减少重复的代码
- 父类中所有非静态的成员属性都会被子类继承
- 父类中的私有成员属性无法被子类访问,但是确实被继承了,只是被编译器所隐藏了
权限修饰符
public
:公共继承
pritected
:保护继承
private
:私有继承
1 2 3 4 5
| class Father {};
class Child : 权限修饰符 Father {};
|
继承的分类
公共继承
- 公共继承的子类,无法继承父类的private成员属性
- 公共继承的子类,继承到的public成员属性还是public权限
- 公共继承的子类,继承到的protected成员属性还是protected权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Father { public: int a; protected: int b; private: int c; };
class Child : public Father { public: int a; protected: int b; };
|
保护继承
- 保护继承的子类,无法继承父类的private成员属性
- 保护继承的子类,继承到的public成员属性和protected成员属性都变为protected权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Father { public: int a; protected: int b; private: int c; };
class Child : protected Father { protected: int a; int b; };
|
私有继承
- 保护继承的子类,无法继承父类的private成员属性
- 保护继承的子类,继承到的public成员属性和protected成员属性都变为private权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class Father { public: int a; protected: int b; private: int c; };
class Child : private Father { private: int a; int b; };
|
报告单个类布局
<class>
:类名
<file>
:C++源码文件
1
| cl /d1 reportSimgleClassLayout<class> <file>
|
访问父子类同名属性和函数
- 子类对象可以直接访问到之类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数(无论是否是重载的成员函数),如果想访问到父类中被隐藏的同名成员函数,需要强制加作用域
非静态成员属性和函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Father { int id; void method() {} void method(int) {} };
class Son : public Father { int id; void method() {} };
int main() { Son son; son.id; son.Father::id; son.method(); son.Father::method(); son.Father::method(1); return 0; }
|
静态成员属性和函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| class Father { static int id; void method() {} }; int Father::id =1;
class Son : public Father { static int id; void method() {} }; int Son::id =1;
int main() { Son son; son.id; son.Father::id; Son::id; Son::Father::id; son.method(); son.Father::method(); Son::method(); Son::method(); return 0; }
|
多继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Father { int id; };
class Mother { int id; };
class Son : public Father, public Mother {
};
|
菱形继承
- 菱形继承,又称钻石继承
- 两个子类继承同一个父类,一个孙子类又继承这两个子类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class GrandFather { int id; };
class Father : public GrandFather { int id; };
class Mother : public GrandFather { int id; };
class Son : public Father, public Mother { int id; };
int main() { Son son; son.Father::id; son.Method::id; }
|
虚继承
- 为了解决菱形继承遇到的问题,引出了虚继承
- 菱形继承遇到的问题:孙子类中包含了重复的同名成员属性,出自于两个父类,这两个父类由于继承自相同的爷爷类,所以同名成员属性传递给孙子类的时候,造成了资源的浪费
- 虚继承采用virtual关键字修饰基类,这种基类成为虚基类
- 虚继承能解决资源的浪费,采用了虚继承,继承时重复的同名成员属性会变成共享的成员属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class GrandFather { int id; };
class Father : virtual public GrandFather { int id; };
class Mother : virtual public GrandFather { int id; };
class Son : public Father, public Mother { int id; };
int main() { Son son; son.Father::id; son.Method::id; }
|
多肽
多肽的分类
虚函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Father { virtual void method() {} };
class Son : public Father { void method() {} };
int main() { Father father = new Son(); father.method(); return 0; }
|
纯虚函数和抽象类
- 通常父类中虚函数的实现是无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
- 包含纯虚函数的类,就是抽象类,抽象类无法实例化对象,继承抽象类的子类必须重写纯虚函数,如果不重写纯虚函数,那么子类也是抽象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Father { virtual void method() = 0; };
class Son : public Father { void method() {} };
int main() { Father father = new Son(); father.method(); return 0; }
|
虚析构函数
- 如果子类中有属性开辟堆区,父类指针无法在释放时调用到子类的析构函数,如果想通过父类调用子类虚构函数,需要采用虚析构函数
- 虚析构函数与普通的析构函数不同,父类的虚析构函数在被子类调用时,父类的虚析构函数和子类的析构函数都会被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Father { vertial ~Father() {} };
class Son : public Father { ~Son() { ... } };
int main() { Father *father = new Son(); return 0; }
|
纯虚析构函数
- 如果只是用父类释放子类堆中的属性,那么父类的虚析构不需要函数体,此时可以使用纯虚析构函数
- 虽然父类析构函数有时只是释放子类堆中的属性,不需要函数体,但是父类的虚析构函数仍然有可能释放父类堆中的属性,所以仍然要定义父类析构函数的函数体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Father { vertial ~Father() = 0; };
Father::~Father() { ... }
class Son : public Father { ~Son() { ... } };
int main() { Father *father = new Son(); return 0; }
|
完成
参考文献
哔哩哔哩——黑马程序员