红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【C++】类与对象

类与对象

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#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;
}

```

此种情况下,编译器使用了默认拷贝构造,进行了值传递

```c++
#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;
}

```

此种情况下,由于有有参函数,编译器不会提供默认无参函数,同时会报错没有默认无参函数

```c++
#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 
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 <<"访问公共属性:" <sitting_room<bedroom<

运算符重载

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

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

可以通过编译器提供的通用名称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
#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成员函数

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