这里介绍类的结构体、联合体以及枚举类

面向对象的基本概念见此

  • 抽象(数据抽象、代码抽象)(类)
  • 封装(接口、安全性)({})
  • 继承 (重用,提高开发效率)
  • 多态(同一名称不同功能,减少标识符数量)(重载函数、虚函数)

一、设计类

面向对象的方法就是利用抽象、封装等机制,借助于对象、类、继承、消息传递等概念进行软件系统构造的软件开发方法。

设计一个类时候需要考虑的方面:

数据、功能、操作符

此类型的“合法值”是什么(数据抽象)

该类型应该有什么样的函数和操作符(代码抽象)

生命周期

类型的对象改如何被创建以及销毁

如何对对象进行初始化以及赋值

消息

对象作为函数的参数如何以值传递

谁讲使用此类型的成员

二、语法

类定义的语法形式,推荐以下形式:

2.1Class

  1. class 类名称
  2. {
  3. public:
  4. 公有成员(首先把外部接口暴露)
  5. private:
  6. 私有成员(除友元外外部函数不可访问(但是成员函数可以访问参数对象的私有变量)) //类内初始化从C++11开始支持。
  7. protected:
  8. 保护型成员(给继承和派生对象共享的)
  9. }; //注意不要忘记分号

2.2 Struct

当然也可以用关键词struct,跟class是完全等价的,除了一点:class不声明保护类型是private的,二struct是public的。如果知识想要一个简单的结构体(全部数据成员都是公共成员,并且没有用户定义的构造函数,没有基类和虚函数(基类和虚函数将在后面的章节中介绍),这个结构体的变量可以用下面的语法形式赋初值),也可以这样写:

  1. struct A{
  2. string name;
  3. int age;
  4. double money;
  5. }a={"dylan",22,222.2323};
  6. //也可以在主函数中顺序赋值
  7. A stu = { "Lin Lin", 19,200 };

结构体存在的主要原因:与C语言保持兼容

定义主要用来保存数据、而没有什么操作的类型一般用struct

2.3 Union

union叫做联合体,与类和结构体都很像(更像结构体,因为默认访问权限是public的),根本差别在于它适用于数据公用,所有成员会公用相同的存储单元,任意两个成员不会同时有效。

  1. union Mark{
  2. char grade; //等级制成绩
  3. bool pass; //只记录是否通过
  4. int percent; //百分制成绩
  5. };// Mark的存储大小取决于最大的成员类型,这里是 int (4字节)

无名联合

一般用于类中,类似结构体的用法。

  1. union{
  2. int i;
  3. double d;
  4. };
  5. int main(){
  6. i= 10;
  7. d = 2.2
  8. } //i的值被冲掉了,因为i和d公用存储空间

一般来可以认为union就是一个提供选择的数据结构,看一下代码了解枚举如何跟union配合使用:

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. class ExamInfo {
  5. private:
  6. string name; //课程名称
  7. enum { GRADE, PASS, PERCENTAGE } mode;//计分方式
  8. union {
  9. char grade; //等级制的成绩
  10. bool pass; //只记是否通过课程的成绩
  11. int percent; //百分制的成绩
  12. };
  13. public:
  14. //三种构造函数,分别用等级、是否通过和百分初始化
  15. ExamInfo(string name, char grade)
  16. : name(name), mode(GRADE), grade(grade) { }
  17. ExamInfo(string name, bool pass)
  18. : name(name), mode(PASS), pass(pass) { }
  19. ExamInfo(string name, int percent)
  20. : name(name), mode(PERCENTAGE), percent(percent) { }
  21. void show();
  22. }
  23. void ExamInfo::show() {
  24. cout << name << ": ";
  25. switch (mode) {
  26. case GRADE: cout << grade; break;
  27. case PASS: cout << (pass ? "PASS" : "FAIL"); break;
  28. case PERCENTAGE: cout << percent; break;
  29. }
  30. cout << endl;
  31. }
  32. int main() {
  33. ExamInfo course1("English", 'B');
  34. ExamInfo course2("Calculus", true);
  35. ExamInfo course3("C++ Programming", 85);
  36. course1.show();
  37. course2.show();
  38. course3.show();
  39. return 0;
  40. }
  41. //运行结果:
  42. //English: B
  43. //Calculus: PASS
  44. //C++ Programming: 85

2.4 枚举类

枚举类是强类型枚举

enum class 枚举类型名: 底层类型 {枚举值列表};

  1. enum Weekday{SUN,MON,TUE,WED,THU,FRI,SAT }; //弱类型枚举,实际上是int型
  2. enum class Type { General, Light, Medium, Heavy}; //默认int型
  3. enum class Category { General=1, Pistol, MachineGun, Cannon}; //与弱类型一样的默认值规则
  4. enum class Type: char { General, Light, Medium, Heavy}; //char类型

枚举类的好处:

  • 强作用域,其作用域限制在枚举类中。避免重名干扰。
    例:使用Type的枚举值General:
    Type::General

  • 转换限制,枚举类对象不可以与整型隐式地互相转换。

  • 可以指定底层类型

2.5 类的成员函数

一般:在类中声明函数,在类外定义函数,函数名前要加上类的作用域

内联函数:在类中定义函数

  1. 或者同一般情况,到那时在函数名外还要加上inline关键词

三、生命周期函数

构造函数,析构函数

其他还有:(拷贝构造函数,拷贝赋值函数),(移动构造函数,移动赋值函数)

3.1构造函数 constructor

在对象被创建时使用特定的值构造对象,将对象初始化为一个特定状态。

  1. 函数名与类名相同,不能定义返回值类型,也不能有return语句
  2. 处理完初始化列表之后,再执行构造函数的函数体。
  3. 类名(成员形参): 成员形参(值)... {};
  • 可以是内联函数

  • 可以重载

  • 可以带默认值参数(当参数全部都有默认值的时候,注意可能跟默认构造函数产生二义性)

合成默认构造函数

类 Class/Struct - 图1 注意:定义在函数体之内的内置类型和复合类型,其值均是未定义的,随机的,而对于类类型string,自动执行其构造函数,初始化为空。大概可以理解为这个默认构造函数啥事不干。

一般我们在自定义带参数构造函数之后,还需要一个无参的默认构造函数。以方便当其他人简单实用此类的时候可以简单生成对象。

  1. #include <iostream>
  2. #include <string>
  3. using namespace std;
  4. //直接定义函数
  5. void func0(string n){cout << n;}
  6. //简单类
  7. class Clock
  8. {
  9. public:
  10. Clock():hour(0),mineut(0),second(0){}; //默认初始化,提高稳健性,和协同性。
  11. Clock(int mH,int mM,int mS);
  12. void showTime();
  13. private:
  14. int hour, mineut, second;
  15. }; //这个分号必须有
  16. //类函数的定义
  17. Clock::Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){}; //这个分号可有可无
  18. void Clock::showTime(){
  19. std::cout<<this->hour<<":"<<this->mineut<<":"<<this->second<<std::endl;
  20. }
  21. //简单数据结构
  22. struct A{
  23. string name;
  24. int age;
  25. double money;
  26. }a={"dylan",22,222.2323}; //这个分号必须有
  27. //主函数
  28. int main()
  29. {
  30. int n = 0;
  31. func0(a.name);
  32. Clock mTime(12,38,22);
  33. Clock defaultTime;
  34. mTime.showTime();
  35. defaultTime.showTime();
  36. return 0;
  37. }

委托构造函数

当重载的构造函数只是参数表和初始化列表不一样,但是算法是一致的时候,可以让代码更加优雅,保持代码一致性,避免代码重复。

针对上述的代码段,使用委托构造函数优化

  1. //之前版本
  2. Clock():hour(0),mineut(0),second(0){};
  3. Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){};
  4. //使用委托构造函数
  5. Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){};
  6. Clock():Clock(0,0,0){};

委托的写法相当于打包转手给成熟的解决方案,非常妙。

3.2析构函数 destructor

在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。

析构函数语法:

  1. ~类名(){/*……*/};

析构函数没有返回类型和形参表

合成默认析构函数

什么也不做,跟合成默认构造函数一样,是处于语法要求必须存在的,没法禁止。

3.3拷贝构造函数

如何用一个已经存在的对象去初始化新的对象,(拷贝构造函数,拷贝赋值函数),(移动构造函数,移动赋值函数)都可以,这里先介绍拷贝构造函数。

  1. 形式:拷贝构造函数与构造函数不同在于其形参是本类对象的const 引用(节省值传递导致的对已经存在对象拷贝所产生的内存消耗)。

拷贝构造函数被调用的三种情况:

  • 定义一个对象时,以本类另一个对象作为初始值,发生复制构造;

  • 虚实传参:如果某函数的形参是类类型,调用函数时,将使实参对象初始化形参对象,发生复制构造;

  • 如果函数的返回值是类类型,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。

    • 这种情况也可以通过移动构造避免不必要的复制

合成默认拷贝构造函数 (浅拷贝)

作用:两个对象的数据成员的一一对应复制。(所以需要传入的是本类对象的引用)

某个例子,经过浅复制,a和b的data指针都指向了相同的字符串地址,这在程序结束的析构中会发生析构两次的情况,因而产生问题。

遇到成员数据中有指针的情况,需要深拷贝。

禁止拷贝构造函数

如果不希望对象被复制:

  • C++98 style:将复制构造函数声明为private且不定义它
  • C++11 style:用=delete静止合成默认拷贝构造函数

四、类的组合

我们可以把类类型看成是基本类型的打包,(当然是被赋予了意义的基本类型,一同打包的还有共类的行为)

类(组合类)中的成员是另一个类(部件类)的对象。 就像是嵌套在一起了,类似继承。

提升程序的抽象性!

4.1 类组合的构造函数设计

当使用组合类的时候,我们需要特别注意部件类的初始化问题。

  1. 原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。

初始化次序:首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中声明的次序

初始化列表中未出现的成员对象,调用默认构造函数(即无形参的)初始化。详见

  1. #include <iostream>
  2. #include <cmath>
  3. using namespace std;
  4. class Point { //Point类定义
  5. public:
  6. Point(int xx = 0, int yy = 0) {
  7. x = xx;
  8. y = yy;
  9. }
  10. Point(Point &p);
  11. int getX() { return x; }
  12. int getY() { return y; }
  13. private:
  14. int x, y;
  15. };
  16. Point::Point(Point &p) { //复制构造函数的实现
  17. x = p.x;
  18. y = p.y;
  19. cout << "Calling the copy constructor of Point" << endl;
  20. }
  21. //类的组合
  22. class Line { //Line类的定义
  23. public: //外部接口
  24. Line(Point xp1, Point xp2);
  25. Line(Line &l);
  26. double getLen() { return len; }
  27. private: //私有数据成员
  28. Point p1, p2; //Point类的对象p1,p2
  29. double len;
  30. };
  31. //组合类的构造函数
  32. Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
  33. cout << "Calling constructor of Line" << endl;
  34. double x = static_cast<double>(p1.getX() - p2.getX());
  35. double y = static_cast<double>(p1.getY() - p2.getY());
  36. len = sqrt(x * x + y * y);
  37. }
  38. Line::Line (Line &l): p1(l.p1), p2(l.p2) {//组合类的复制构造函数
  39. cout << "Calling the copy constructor of Line" << endl;
  40. len = l.len;
  41. }
  42. //主函数
  43. int main() {
  44. Point myp1(1, 1), myp2(4, 5); //建立Point类的对象
  45. Line line(myp1, myp2); //建立Line类的对象
  46. Line line2(line); //利用复制构造函数建立一个新对象
  47. cout << "The length of the line is: ";
  48. cout << line.getLen() << endl;
  49. cout << "The length of the line2 is: ";
  50. cout << line2.getLen() << endl;
  51. return 0;
  52. }

这里一共会输出6次Point类的拷贝构造函数信息以及1次Line类的拷贝构造信息。

一般来说,构造组合类时先构造各个部件类,才是组合类。析构组合类时,先析构组合类再析构部件类。(用嵌套去理解更形象:最先构造内部,然后嵌套;最先拆除嵌套然后析构内部)

4.2 前向引用声明

类跟函数一样,应该先声明后使用。

如果需要在某个类声明之前就要引用该类,(比如两个类互相引用彼此),就要前向引用声明

前向引用声明只为程序提供一个标识符,具体声明在别的地方。

所以只能使用之歌标识符,而不能使用它的对象。
//正确
struct B;
struct A{void f(B b);};
struct B{void f(A a);};

//错位
struct Fred;
struct Barney{Fred f;};  // Error
struct Fred{Barney b;};
//正确的修改
struct Fred; 
struct Barney{Fred *f;};
struct Fred {Barney b;};