【C++】类与对象

  • ~26.39K 字
  1. 1. 类与对象
    1. 1.1. 封装
      1. 1.1.1. 封装的实现
      2. 1.1.2. 权限
      3. 1.1.3. struct与class的区别
      4. 1.1.4. 成员属性私有化
    2. 1.2. 对象的初始化和清理
      1. 1.2.1. 构造函数与析构函数
      2. 1.2.2. 构造函数的分类与调用
      3. 1.2.3. 拷贝构造函数调用时机
      4. 1.2.4. 构造函数调用规则
      5. 1.2.5. 深拷贝与浅拷贝
      6. 1.2.6. 初始化列表
      7. 1.2.7. 类对象作为类成员
      8. 1.2.8. 静态成员
    3. 1.3. C++对象模型与this指针
      1. 1.3.1. 成员变量与成员函数分开存储
      2. 1.3.2. this指针
      3. 1.3.3. 空指针访问成员函数
      4. 1.3.4. const修饰成员函数
      5. 1.3.5. 类的特殊成员函数
      6. 1.3.6. 可平凡复制类
      7. 1.3.7. 标准布局类
    4. 1.4. 友元
      1. 1.4.1. 全局函数做友元
      2. 1.4.2. 类做友元
      3. 1.4.3. 成员函数做友元
    5. 1.5. 运算符重载
      1. 1.5.1. 加号运算符重载
      2. 1.5.2. 左移运算符重载
      3. 1.5.3. 递增运算符重载
      4. 1.5.4. 赋值运算符重载
      5. 1.5.5. 关系运算符重载
      6. 1.5.6. 函数调用运算符重载
    6. 1.6. 继承
      1. 1.6.1. 继承方式
      2. 1.6.2. 继承中的对象模型
      3. 1.6.3. 继承中的构造与析构顺序
      4. 1.6.4. 同名成员处理方式
      5. 1.6.5. 多继承
      6. 1.6.6. 菱形继承
    7. 1.7. 多态
      1. 1.7.1. 分类
      2. 1.7.2. 纯虚函数与抽象类
      3. 1.7.3. 虚析构与纯虚析构
    8. 1.8. 杂项
      1. 1.8.1. override与final限定符
      2. 1.8.2. const成员函数

类与对象

C++面向对象的三大特性:封装、继承、多态

C++中,任何事物都为对象,对象有其属性和行为

具有相同性质的对象,我们可以抽象称为类

封装

封装的实现

意义:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法:class <类名>{ <访问权限>: <属性/行为>}

一些名词:

  • 成员:类中的属性和行为的统称
  • 成员属性(成员变量):类中的属性
  • 成员函数(成员方法):类中的函数

e.g.1 设计一个圆类,求其周长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
// 设计一个圆类,求其周长
const double PI = 3.14;
class circle{
// 访问权限: 公共权限
public:
// 属性:半径
int r;
// 行为:获取周长
double calC(){
return 2*PI*r;
}
};
int main(){
// 通过类创建一个对象——实例化
circle c1;
// 给对象的属性进行赋值,使用.
c1.r = 10;
// 调用行为
cout << "r="<< c1.calC()<<endl;

return 0;
}

e.g.2 设计一个学生类,属性有姓名和学号,可以赋值并显示姓名和学号

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
#include <iostream>
#include <string>
using namespace std;
// 设计一个学生类,有姓名和学号,可以赋值并显示
class student{
public:
//属性
string name; // 姓名
int id; // 学号

// 行为
void display(){
cout << "name:" << name<<endl;
cout << "id:" << id <<endl;
}

void set_info(string s_name, int s_id){
name = s_name;
id = s_id;
}
};

int main(){
student stu1;// 实例化
student stu2;
// 一般赋值
stu1.name = "Jack";
stu1.id = 1;
stu1.display();
// 通过行为赋值
stu2.set_info("Mary", 2);
stu2.display();
return 0;
}

权限

类可以将属性和方法放在不同权限下:

  • public:公共权限,成员类内可以访问,类外也可以访问
  • protected:保护权限,成员类内可以访问,类外不可以访问,子类可以访问父类的保护成员
  • private:私有权限,成员类内可以访问,类外不可以访问, 子类不可访问父类的私有成员
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
#include <iostream>
#include <string>
using namespace std;
class person{
public: //公共权限
string name;
protected: // 保护权限
string car;
private: // 私有权限
int password;
public:
// 类内可以访问
void f(){
name = "Jack";
car = "xiaomisu7";
password = 123456;
}
};
int main(){
person p;
p.name = "Mary";
//p.car = "?" // protected 类外不可访问
//p.password = 123 // private 类外不可访问
return 0;
}

struct与class的区别

C++中class与struct的唯一区别在于默认的访问权限不同

  • class默认权限私有
  • struct默认权限公共

成员属性私有化

优点:

  • 可以自己控制读写权限
  • 可以检测数据有效性

基本思路:用公有方法对私有属性进行操作

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
#include <iostream>
#include <string>
using namespace std;

class person{
// 属性私有
private:
string name; // 姓名:可读可写
int age = 18; // 年龄:只读
string hobby; // 爱好:只写
// 方法公有
public:
// 设置姓名
void setName(string s){
name = s;
}
// 获取姓名
void getName(){
cout <<"name:"<<name<<endl;
}
// 获取年龄
void getAge(){
cout <<"age:"<<age<<endl;
}
// 设置爱好
void setHobby(string s){
hobby = s;
}
};
int main(){
person p;
p.setName("Jack");
p.getAge();
p.setHobby("jerk");

return 0;
}

对象的初始化和清理

对象的初始化和清理是很重要的安全问题:

  • 一个对象或变量没有初始状态,对其使用后果未知
  • 使用完一个对象或变量,没有及时清理,也会造成安全问题

构造函数与析构函数

C++提供了这两种函数,被编译器自动调用,完成对象初始化与清理工作

这两个工作是强制执行的,若我们不提供,编译器会提供自己的构造函数与析构函数,他们是空实现

构造函数:用于创建对象时为对象成员赋值

  • 语法:<类名>(){}

  • 无返回值,不用写void

  • 函数名与类名相同

  • 构造函数可以有参数,可以发生重载

  • 调用对象时会自动调用构造,只会调用一次

析构函数:用于对象销毁前自动调用,执行清理操作

  • 语法:~<类名>(){}
  • 无返回值,不用写void
  • 函数名与类名相同,名前面加~
  • 析构函数不可以有参数,不可发生重载
  • 程序在对象销毁前会自动调用析构。只会调用一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

class person{

public:
// 构造函数
person(){
cout << "构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
};

int main(){
person 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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;

class person{

public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};

int main(){
// 括号法
person p; // 默认构造函数被调用(不加括号)
person p1(10); // 有参构造函数被调用
person p2(p1); // 拷贝构造函数被调用
cout << "age of p1:" << p1.age << endl;
cout << "age of p2:" << p2.age << endl;
// 显式法
person p3 = person(20);
person p4 = person(p3);

// 隐式转换法
person p5 = 10; // 相当于 person p5 = person(10);
person p6 = p5; // 相当于 person p6 = person(p5);
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
45
46
47
48
49
#include <iostream>
using namespace std;

class person{

public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};

// 使用已经创建完毕的对象来初始化一个新对象
void test1(){
person p1(20);
person p2(p1);
}
// 值传递方式给函数参数传值
void test2(person p){
return ;
}
// 值传递返回局部对象
person test3(){
person p1;
return p1;
}

int main(){
person p1;
test1();
test2(p1);
person p2 = test3();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
无参构造函数被调用
----------------
有参构造函数被调用
拷贝构造函数被调用
析构函数被调用
析构函数被调用
----------------
拷贝构造函数被调用
析构函数被调用
----------------
无参构造函数被调用
析构函数被调用
析构函数被调用

构造函数调用规则

默认下,编译器至少给一个类添加三个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性值拷贝

调用规则:

  • 若用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数 ,C++不再提供其他构造函数
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
#include <iostream>
using namespace std;

class person{

public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};

int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
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
#include <iostream>
using namespace std;

class person{

public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// 有参构造
person(int a){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};

int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
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
#include <iostream>
using namespace std;

class person{

public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// // 有参构造
// person(int a){
// age = a;
// cout << "有参构造函数被调用" << endl;
// }
// 拷贝构造
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person(){
cout << "析构函数被调用" << endl;
}
int age;
};

int main(){
person p;
p.age = 18;
person p2(p);
cout << "p2的年龄是:" << p2.age << endl;
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
#include <iostream>
using namespace std;

class person{

public:
// 无参构造(默认构造)
person(){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person(int a, int iheight){
age = a;
height = new int(iheight);
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person(){
// 在析构函数中进行堆区数据释放
if (height!=NULL){
delete height;
height = NULL;
}
cout << "析构函数被调用" << endl;
}
int age;
int* height; // 身高,使用指针将数据创建在堆区
};

int main(){
person p1(18,160);
cout << "p1的年龄是:" << p1.age << endl;
cout << "p1的身高是:" << *p1.height << endl;
person p2(p1);
cout << "p2的年龄是:" << p2.age << endl;
cout << "p2的身高是:" << *p2.height << endl;
return 0;
}

该代码会卡住,原因是利用拷贝构造只进行了值传递,p1与p2执行完毕后,均执行析构函数,对height指向的空间进行释放,但是其中一个已经执行释放后,另一个再次释放会产生非法操作,故出错

这也就是浅拷贝的缺点:只进行了值传递,我们需要利用深拷贝来结局

1
2
3
4
5
6
7
// 拷贝构造 
person(const person &p){
age = p.age; // 将传入数据拷贝到当前对象
// 深拷贝,在堆区开辟新的空间
height = new int(*p.height);
cout << "拷贝构造函数被调用" << endl;
}

即在拷贝构造中,重新给拷贝的指针变量申请一块空间

如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝出现问题

初始化列表

C++提供了初始化列表来初始化属性

语法:<构造函数>(): <属性1>(<值1>), <属性2>(<值2>) ... {}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class person{

public:
int a;
int b;
int c;
// 初始化列表赋值
person(int a, int b, int c):a(10), b(20), c(30){
}
};

int main(){
person p;
cout << "a:" << p.a << endl;
cout << "b:" << p.b << endl;
cout << "c:" << p.c << endl;
return 0;
}

类对象作为类成员

C++中的成员可以是另一个类的对象,称为对象成员

如

1
2
3
4
class A{};
class B{
A a;
};

该B类中有对象A作为成员,则A为对象成员

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
#include <iostream>
#include <string>
using namespace std;
class phone{
public:
string brand;
float price;

phone(string pname){
brand = pname;
cout << "phone构造函数被调用" << endl;
}
~phone(){
cout << "phone析构函数被调用" << endl;
}
};

class person{

public:
string name;
phone iphone;

person(string pname, string pbrand):name(pname),iphone(pbrand){
//相当于phone iphone = pname;的隐式转换
cout<< "person构造函数被调用" << endl;
}
~person(){
cout << "person析构函数被调用" << endl;
}

};
int main(){
person p("小明","苹果");
cout << "姓名:" << p.name << endl;
cout << "手机品牌:" << p.iphone.brand << endl;
return 0;
}
1
2
3
4
5
6
phone构造函数被调用
person构造函数被调用
姓名:小明
手机品牌:苹果
person析构函数被调用
phone析构函数被调用

注意构造函数的调用顺序:

  • 当其他类对象作为本类成员,构造时先构造类对象,在构造自身
  • 当其他类对象作为本类成员,析构时先析构自身,再析构类对象

静态成员

在成员变量与成员函数前加上关键字static,称为静态成员

分类:

  • 静态成员变量
    • 不属于某个对象,所有对象共享一份数据
    • 编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有成员共享同一个函数
    • 静态成员函数只能访问静态成员变量
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
#include <iostream>
using namespace std;

class person{

public:
static int a; // 需要类内声明,类外初始化
int c;
static void f(){
cout << "静态成员函数f调用"<<endl;
a = 100;
// c = 200; // 静态成员函数不可以访问非静态成员变量
}
private:
static int b; // 静态成员变量也有权限


};
int person::a = 100; // 在类外初始化,需要指定作用域
int person::b = 100; // 也需要在类外初始化
int main(){
person p; // 栈上的数据,执行完后会被释放,故执行析构函数
cout << p.a<<endl;
person p2;
p2.a = 200; // 数据共享,此处改变即一起改变
cout << p.a<<endl; // 通过对象进行访问
cout << person::a<<endl; // 通过类名进行访问
// cout << p.b<<endl; // 不可访问

// 同样两种访问模式
p.f();
person::f();
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
#include <iostream>
using namespace std;

class Singleton{
private:
static Singleton* m_insntance; // 存储该类的唯一实例
// 将构造与析构设为私有
Singleton(){}
~Singleton(){}
public:
// 删除拷贝构造与赋值运算符构造
Singleton(const Singleton& obj) = delete;
Singleton& operator=(const Singleton&) = delete;

// 创建该唯一实例, 只能通过该方法创建获得对象
static Singleton* getInstance(){
if (!m_insntance){
m_insntance = new Singleton();
}
return m_insntance;
}
private:
string m_name;
public:
void setName(const string& name){
m_name = name;
}
const string& getName(){
return m_name;
}
};
Singleton* Singleton::m_insntance = NULL;
int main(){
// 第一次调用创建实例
Singleton* s1 = Singleton::getInstance();
cout << s1->getName()<<endl;
// 第二次调用使用已经创建的实例
Singleton* s2 = Singleton::getInstance();
cout << s1->getName()<<endl;
}

C++对象模型与this指针

成员变量与成员函数分开存储

c++中,类内成员变量与成员函数分开存储

只有非静态成员变量才属于类的对象上

对于空对象,编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置

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
#include <iostream>
using namespace std;

class person{

};
class person2{
int a;
};
class person3{
int a;
static int b;
void f(){}
};

int main(){
person p;
person2 p2;
person3 p3;
// 占用内存为1
cout << "size of empty class: "<< sizeof(p)<<endl;

// 占用内存为4
cout << "size of class person2: "<< sizeof(p2)<<endl;

// 静态成员与非静态成员函数不属于类的对象上,故占用内存也为4
cout << "size of class person3: "<< sizeof(p3)<<endl;
return 0;
}

this指针

C++通过提供this指针解决了区分多个对象调用同一块代码分不清的问题

this指针指向被调用的成员函数所属的对象,不需要定义,直接使用

用途:

  • 形参和成员变量同名时,可以用this指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用retrun *this
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
#include <iostream>
using namespace std;

class person{

public:
int age;
person(int age){
// 当形参和成员变量同名时,this 指针指向被调用成员函数所属对象
this->age = age; // 此时this->age和定义的成员变量age是同一个
}
// 成员函数返回对象本身
person& addage(person &p){
this->age+=p.age;
return *this;
}
};

int main(){
// 解决名称冲突
person p(18);
person p2(10);
cout <<"age:" <<p.age<<endl;

// 返回对象本身可以实现链式调用
p2.addage(p).addage(p).addage(p);
cout <<"age of p2:"<<p2.age<<endl;

return 0;
}

空指针访问成员函数

C++允许空指针调用成员函数,但是要注意有没有用到this指针

如果用到this指针,要注意保证代码的鲁棒性

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
#include <iostream>
using namespace std;

class person{

public:
int age;
void show(){
cout <<"this is person class"<<endl;
}
void showage(){
// 防止指向空
if (this==NULL){
return ;
}
// 调用成员变量时,默认在前面加this->,但此时p是空指针,故指向的是空对象,this没有指向数据
cout << "age = "<<age <<endl;
}
};

int main(){
person *p = NULL;
p->show();
p->showage();
return 0;
}

const修饰成员函数

常函数:

  • 成员函数后加const称该函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数内依然可以修改

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象构造时需要对内部成员变量初始化(构造函数)
  • 常对象只能调用常函数
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
#include <iostream>
using namespace std;

class person{

public:
int age;
mutable int b; // 加上mutable也可以在常函数中修改
// this指针本身是指针常量,指针指向是不可修改的,指针指向的值是可以修改的
// 而加上const修饰后,修饰的是this指向的值,让指向的值也不可修改
// 相当于const person * const this;
void show() const{
//this->age = 100; // 此时不可修改
this->b = 100;
}
void f(){}
person(){

}
};

int main(){
// 常对象
const person p;
// p.age = 100; // 常对象下不能修改值
p.b = 100; // 但有mutable的可以修改
// p.f() // 常对象只能调用常函数
p.show();
return 0;
}

类的特殊成员函数

特殊成员函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,他们均可以用以下两个关键字修饰

  • =delete:

    可以在函数声明后添加 =delete来删除当前函数,如删除构造函数、删除不需要的传参类型

  • =default:

    加在特殊函数后,可以用来声明一个默认特殊函数,还可以自动使用默认值初始化变量

可平凡复制类

C++中,各种数据类型及他们的数组、指针、枚举等在内存中是连续存储的,可以使用memcpy()函数进行拷贝,称为可平凡复制类型

一个类若满足以下条件,被称为可平凡复制类,也是一种可平凡复制类型

  • 没有虚函数
  • 没有用户自定义的特殊成员函数
  • 有自动生成的析构函数
  • 所有非静态成员变量也是可平凡复制类型
  • 拷贝、移动构造函数,拷贝、移动赋值运算符中至少有一个是未被删除的
  • 对于继承类,其父类要满足上述条件

可以使用<type_traits>库中的is_trivially_copyable<类>::value来判断是否可拷贝

标准布局类

C++中,所有标量类型都是标准布局类型

对一个非继承类,满足以下条件被称为标准布局类

  • 没有虚函数
  • 所有类成员都具有相同访问属性
  • 所有类成员都是标准布局类型
  • 若类是继承类,则满足该类和父类中只有一个类有非静态数据成员,且类型与访问控制满足上述条件

可以使用<type_traits>库中的is_standard_;leyout<类>::value来判断是否可拷贝

友元

友元可以让一个函数或者类访问另一个类中的私有成员

友元的关键字为friend

全局函数做友元

在class中声明对应全局函数,并在最前面加friend关键词,可以声明友元

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
#include <iostream>
using namespace std;

class building{
friend void fir(building &building); // 声明该全局函数为building类的友元
public:
string sitting_room;
private:
string bedroom;

public:
building(){
sitting_room = "客厅";
bedroom = "卧室";
}
};
void fir(building &building){
cout << "访问公共属性:"<<building.sitting_room<<endl;
cout << "访问私有属性:"<<building.bedroom<<endl;

}
int main(){
building b;
fir(b);
}

类做友元

在class中声明对应类,并在最前面加friend关键词,可以声明友元,被声明的类可以访问原类中的私有属性

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
#include <iostream>
using namespace std;
class buildings;
class fri{
public:
buildings* building;
fri();
void visit();
};

class buildings{
friend class fri; // 声明fri类是building类的友元
public:
string sitting_room;
buildings();
private:
string bedroom;
};

// 在类外写成员函数
buildings::buildings(){
sitting_room = "客厅";
bedroom = "卧室";
}
fri::fri(){
building = new buildings;
}

void fri::visit(){
cout <<"访问公共属性:" <<building->sitting_room<<endl;
cout <<"访问私有属性:" <<building->bedroom<<endl; // 友元可以访问类中的私有属性
}

int main(){
fri f;
f.visit();
}

成员函数做友元

在class中声明对应类中的成员函数,并在最前面加friend关键词,可以声明友元,被声明的成员函数可以访问原类中的私有属性

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
#include <iostream>
using namespace std;
class buildings;
class fri{
public:
buildings* building;
fri(){
building = new buildings;
}
void visit(); // 让visit可以访问buildings中的私有属性
};

class buildings{
friend void fri::visit(); // 声明fri类下的visit成员函数是building类的友元
public:
string sitting_room;
buildings(){
sitting_room = "客厅";
bedroom = "卧室";
}
private:
string bedroom;
};

void fri::visit(){
cout <<"访问公共属性:" <<building->sitting_room<<endl;
cout <<"访问私有属性:" <<building->bedroom<<endl; // 友元可以访问类中的私有属性
}

int main(){
fri f;
f.visit();
}

运算符重载

运算符重载可以对已有的运算符重新定义,赋予另一种功能以适用于不同的数据类型

如我们通过写成员函数来实现两个类相加属性后返回新的对象

可以通过编译器提供的通用名称operator来重载运算符以简化运算

重载的本质调用为(以person类为例):

  • 成员函数:person p3 = p1.operator<运算符>(p2)
  • 全局函数:person p3 = p1.operator<运算符>(p1, p2)

注意:

  • 运算符重载也可以实现函数重载
  • 对于内置数据类型的表达式的运算符是不可以重载的
  • 不要滥用运算符重载

加号运算符重载

成员函数:

1
2
3
4
5
6
person operator+(person &p){
person temp;
temp.a = this->a+p.a;
temp.b = this->b+p.b;
return temp;
}

全局函数:

1
2
3
4
5
6
person operator+(person &p1, person &p2){
person temp;
temp.a = p1.a+p2.a;
temp.b = p1.b+p2.b;
return temp;
}

左移运算符重载

1
2
3
4
5
6
7
8
// 只能全局重载左移运算符
// 若在类内重载会称为p.operator<<(cout) 即p<<cout (cout是ostream流中的对象)
// 这里先写cout即可实现cout在左边p在右边
// 返回值作为cout本身的引用,可以实现链式编程
ostream & operator<<(ostream &cout, person &p){
cout << "a=" << p.a << endl << "b=" << p.b;
return cout;
}

递增运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 前置++
// 这里返回引用是为了确保只对同一个数据进行操作
myint& operator++(){
num++; // 先进行自增运算
return *this; // 再返回自身
}

// 后置++
// 这里为了防止重复,添加一个int占位符来实现函数重载以区分前后置
myint operator++(int){
myint temp = *this;
num++;
return temp;
}

赋值运算符重载

C++编译器会额外给一个类添加赋值运算符的重载函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;
class person{
public:
int *age;
person(int age){
this->age = new int(age); // 年龄放在堆区
}

// 将age内存释放
~person(){
if (age!=NULL){
delete age;
age = NULL;
}
}
};
int main(){
person p1(1);
cout << "p1's age:" << *p1.age << endl;
person p2(2);
p2 = p1; // 赋值操作
cout <<"p2's age:"<<*p2.age<<endl;
}

该代码出错,由于age开到了同一块堆区内存,故在p1赋值给p2时,两者的age指针都指向同一块区域

则当p2执行完毕,执行析构函数时,将该堆区内存释放;再等p1执行完毕,执行析构函数时,将已经释放的内存重复释放了,故报错

我们需要通过重载赋值运算符来实现深拷贝

1
2
3
4
5
6
7
8
9
10
// 重载赋值运算符
person& operator=(const person &p){
// 首先判断是否有属性在堆区,若有先释放,再深拷贝
if (age!=NULL){
delete age;
age = NULL;
}
age = new int(*p.age);
return *this;
}

关系运算符重载

实现自定义数据类型的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重载==运算符
bool operator==(const person &p){
if (this->name==p.name && this->age==p.age){
return true;
}
else return false;
}

// 重载!=运算符
bool operator!=(const person &p){
if (this->name==p.name && this->age==p.age){
return false;
}
else return true;
}

大于小于类似

函数调用运算符重载

  • 函数调用运算符即()
  • 重载后的使用方式很像函数的调用,故也称为仿函数
  • 仿函数没有固定写法
1
2
3
4
5
6
7
8
9
10
11
12
13
class print{
public:
//重载函数调用运算符
void operator()(string s){
cout << s << endl;
}
};
class add{
public:
int operator()(int n1, int n2){
return n1+n2;
}
};

也可以用匿名函数对象进行调用

继承

有些类与其他类之间除了有共性,还有自己的特性,我们可以用继承的技术来减少重复代码优点:减少重复代码

语法:<子类>: <继承方式> <父类>

其中子类也称作派生类,父类也称基类

子类中的成员包含两部分:

一类是从基类继承过来的,一类是自己增加的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class page{
public:
void f1()
{
cout << "f1"<<endl;
}
void f2(){
cout << "f2" <<endl;
}
void f3(){
cout << "f3" << endl;
}

};
class page2:public page{
public:
void s1(){
cout << "s1"<<endl;
}
};

继承方式

即:

  • 私有成员子类均不可访问
  • 公共继承下:各成员权限不变
  • 保护继承下:public变为protected
  • 私有继承下:public与protected变为private

继承中的对象模型

父类中所有非静态成员属性都会被子类继承

而私有属性被编译器隐藏,故访问不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
class fa{
public:
int a;
protected:
int b;
private:
int c;
};
class son:public fa{
public:
int d;
};
int main(){
// 结果为16,即父类中所有属性都会被继承
cout << sizeof(son)<<endl;
}

继承中的构造与析构顺序

子类继承父类后,创建子类对象时,也会调用父类的构造函数

顺序:先构造父类,再构造子类,析构相反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
using namespace std;
class base{
public:
base(){
cout <<"base构造函数"<<endl;
}
~base(){
cout <<"base析构函数"<<endl;
}
};
class son:public base{
public:
son(){
cout <<"son构造函数"<<endl;
}
~son(){
cout <<"son析构函数"<<endl;
}
};
int main(){
son s;
}
1
2
3
4
base构造函数
son构造函数
son析构函数
base析构函数

同名成员处理方式

当子类和父类出现同名成员:子类的同名成员会隐藏掉所有父类中同名成员(函数重载也会被隐藏)

  • 访问子类同名成员:直接访问
  • 访问父类同名成员:加作用域

静态与非静态处理方式一样

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
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
using namespace std;
class fa{
public:
int a;
static int b;
fa(){
a = 100;
}
void f(){
cout <<"f in fa"<<endl;
}
static void ff(){
cout <<"ff in fa"<<endl;
}
};
class son:public fa{
public:
int a;
static int b;
son(){
a = 200;
}
void f(){
cout <<"f in son"<<endl;
}
static void ff(){
cout <<"ff in son"<<endl;
}
};
int fa::b = 10;
int son::b = 20;

int main(){
son s;
cout << "a in son:"<<s.a<<endl;
// 加作用域访问父类中的同名成员变量
cout << "a in fa:" <<s.fa::a<<endl;
s.f();
// 加作用域调用父类中的同名成员函数
s.fa::f();

// 非静态同名成员访问:通过对象
cout <<"static b in son:"<<s.b<<endl;
cout <<"static b in fa:"<<s.fa::b<<endl;
s.ff();
s.fa::ff();

// 非静态同名成员访问:通过类名
cout <<"static b in son:"<<son::b <<endl;
// 第一个::代表通过类名访问,第二个::代表访问父类作用域下
cout <<"static b in fa:"<<son::fa::b<<endl;
son::ff();
son::fa::ff();


}

多继承

C++允许一个类继承多个类

语法:<子类>: <继承方式> <父类1>, <继承方式> <父类2>...

同名成员出现时需要加作用域区分

实际开发时不建议使用多继承

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
#include <iostream>
using namespace std;
class fa{
public:
int a;
fa(){
a = 100;
}
void f(){
cout <<"f in fa"<<endl;
}
};
class fa2{
public:
int a;
fa2(){
a= 200;
}
void f(){
cout <<"f in fa"<<endl;
}
};
class son:public fa, public fa2{
public:
int c;
int d;
son(){
c = 300;
d = 400;
}
};
int main(){
// 多继承子类空间
cout << "size of son:" <<sizeof(son)<<endl;

son s;
cout <<"a in fa:"<<s.fa::a<<endl;
cout <<"a in fa2:"<<s.fa2::a<<endl;

}

菱形继承

概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类

菱形继承时,两个父类拥有相同数据,需要加作用域区分

但该数据对于最后的子类来说存在两份,会浪费空间,需要利用虚继承来解决问题

在需要虚继承的部分的访问权限前添加关键字vritual即可,此时对应父类称为虚基类

原理:此时的父类继承的是虚基类指针(vbptr),指向一个虚基类表,该表存储了指针位置以及对应偏移量,根据偏移量便可以找到继承的那唯一一份数据

缺点:虚继承占用内存很大

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
#include <iostream>
using namespace std;
class animal{
public:
int age;
};

// sheep 继承了 animal
class sheep: virtual public animal{};

// camel 继承了 animal
class camel: virtual public animal{};

// alpaca 继承了 sheep和camel
class alpaca: public sheep, public camel{};

int main(){
alpaca st;
// 菱形继承时,两个父类有相同数据,需要加作用域区分
st.sheep::age = 18;
st.camel::age = 20;
cout << "age of sheep: "<<st.sheep::age<<endl;
cout << "age of camel: "<<st.camel::age<<endl;
cout << "age of alpaca: "<<st.alpaca::age<<endl;

}

多态

分类

  • 静态多态:函数和运算符重载属于静态多态,复用函数名;函数地址在编译阶段确定
  • 动态多态:派生类和虚函数实现运行时多态;函数地址在运行阶段确定

### 动态多态

满足条件:

  • 存在继承关系

  • 子类要重写父类的虚函数

    (重写即函数返回值类型、函数名、参数列表完全相同)

使用:父类的指针或者引用指向子类对象

原理:父类记录的也是虚基类指针,指向对应虚基类表。当子类重写后,对应指向的虚基类表被现子类覆盖,得到对应参数

优点:

  • 组织结构清晰
  • 可读性强
  • 前期后期拓展以及维护性高
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
#include<iostream>
using namespace std;

class animal{
public:
// 虚函数
virtual void speak(){
cout <<"speaking"<<endl;
}
};

class cat: public animal{
public:
void speak(){
cout <<"cat is speaking"<<endl;
}
};

// 地址在编译阶段确定函数地址,但为了让多个类输出对应,需要在运行时确定
// 当给animal中的speak变为虚函数后,便可以实现子类重写父类
void dospeak(animal &ani){ // 父类引用
ani.speak(); // 但调用的是animal中的speak函数
}
int main(){
cat cat;
dospeak(cat); // 父类引用指向了子类
}

在开发时,提倡开闭原则:即对扩展进行开放,对修改进行关闭

纯虚函数与抽象类

多态中,通常父类中的虚函数实现是毫无意义的,都是调用子类中的重写内容

因此可以将虚函数改为纯虚函数

语法:virtual <返回值类型> <函数名>([参数列表]) = 0;

此时这个类称为抽象类

  • 抽象类无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
using namespace std;

class base{
public:
virtual void func() = 0; // 纯虚函数
};
class son :public base{
public:
// 子类必须重写父类中的纯虚函数
virtual void func(){
cout <<"func调用"<<endl;

}
};
int main(){
// base b; 不允许实例化抽象类
base *b = new son;
b->func();
}

虚析构与纯虚析构

多态使用时,如果子类中有属性开辟到堆区,则父类指针在释放时就无法调用到子类的虚构代码,此时需要将父类中的析构函数改为虚析构或纯虚析构

  • 可以解决父类指针释放子类对象
  • 都需要有函数具体实现

其中,纯虚析构需要类外实现

有纯虚析构后该类便成为了抽象类

语法:virtual ~<类名>(){}或virtual ~<类名>()=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
#include <iostream>
using namespace std;

class animal{
public:
virtual void speak() = 0;
// 用虚析构解决父类指针释放子类对象时释放不完全的问题
virtual ~animal(){
cout <<"animal析构调用"<<endl;
}
};
class cat: public animal{
public:
cat(string name){
cname = new string(name);
}
virtual void speak(){
cout <<*cname<<"'s speaking"<<endl;
}
~cat(){
if (cname !=NULL){
cout <<"cat析构调用"<<endl;
delete cname;
cname = NULL;
}
}

string *cname;
};

int main(){
animal * ani = new cat("Tom");
ani->speak();
// 父类指针析构时不会调用子类析构函数,导致若子类有析构属性时不会被释放
delete ani;
}

杂项

override与final限定符

在基类是虚函数的情况下,子类对该虚函数进行重写,在要重写的函数后面加上override可以防止因为子类的拼写错误,数据类型错误等问题,导致无法调用到子类而直接调用了基类

(若不相同会直接报错)

在虚函数声明的结尾加上final可以防止该虚函数被子类重写,在类声明的后面加上final可以防止该类被继承

注意这两个限定符不是保留关键字

const成员函数

在成员函数声明和定义的后面加上关键字const

const成员函数不能修改其成员变量,不能调用非const成员函数,不能通过const类型的对象调用非const成员函数

即常量对象只可以调用常量方法

赞助喵
非常感谢您的喜欢!
赞助喵