这里介绍类的结构体、联合体以及枚举类
面向对象的基本概念见此
- 抽象(数据抽象、代码抽象)(类)
- 封装(接口、安全性)({})
- 继承 (重用,提高开发效率)
- 多态(同一名称不同功能,减少标识符数量)(重载函数、虚函数)
一、设计类
面向对象的方法就是利用抽象、封装等机制,借助于对象、类、继承、消息传递等概念进行软件系统构造的软件开发方法。
设计一个类时候需要考虑的方面:
数据、功能、操作符
此类型的“合法值”是什么(数据抽象)
该类型应该有什么样的函数和操作符(代码抽象)
生命周期
类型的对象改如何被创建以及销毁
如何对对象进行初始化以及赋值
消息
对象作为函数的参数如何以值传递
谁讲使用此类型的成员
二、语法
类定义的语法形式,推荐以下形式:
2.1Class
class 类名称
{
public:
公有成员(首先把外部接口暴露)
private:
私有成员(除友元外外部函数不可访问(但是成员函数可以访问参数对象的私有变量)) //类内初始化从C++11开始支持。
protected:
保护型成员(给继承和派生对象共享的)
}; //注意不要忘记分号
2.2 Struct
当然也可以用关键词struct,跟class是完全等价的,除了一点:class不声明保护类型是private的,二struct是public的。如果知识想要一个简单的结构体
(全部数据成员都是公共成员,并且没有用户定义的构造函数,没有基类和虚函数(基类和虚函数将在后面的章节中介绍),这个结构体的变量可以用下面的语法形式赋初值),也可以这样写:
struct A{
string name;
int age;
double money;
}a={"dylan",22,222.2323};
//也可以在主函数中顺序赋值
A stu = { "Lin Lin", 19,200 };
结构体存在的主要原因:与C语言保持兼容
定义主要用来保存数据、而没有什么操作的类型一般用struct
2.3 Union
union叫做联合体,与类和结构体都很像(更像结构体,因为默认访问权限是public的),根本差别在于它适用于数据公用,所有成员会公用相同的存储单元,任意两个成员不会同时有效。
union Mark{
char grade; //等级制成绩
bool pass; //只记录是否通过
int percent; //百分制成绩
};// Mark的存储大小取决于最大的成员类型,这里是 int (4字节)
无名联合
一般用于类中,类似结构体的用法。
union{
int i;
double d;
};
int main(){
i= 10;
d = 2.2
} //i的值被冲掉了,因为i和d公用存储空间
一般来可以认为union就是一个提供选择的数据结构,看一下代码了解枚举如何跟union配合使用:
#include <iostream>
#include <string>
using namespace std;
class ExamInfo {
private:
string name; //课程名称
enum { GRADE, PASS, PERCENTAGE } mode;//计分方式
union {
char grade; //等级制的成绩
bool pass; //只记是否通过课程的成绩
int percent; //百分制的成绩
};
public:
//三种构造函数,分别用等级、是否通过和百分初始化
ExamInfo(string name, char grade)
: name(name), mode(GRADE), grade(grade) { }
ExamInfo(string name, bool pass)
: name(name), mode(PASS), pass(pass) { }
ExamInfo(string name, int percent)
: name(name), mode(PERCENTAGE), percent(percent) { }
void show();
}
void ExamInfo::show() {
cout << name << ": ";
switch (mode) {
case GRADE: cout << grade; break;
case PASS: cout << (pass ? "PASS" : "FAIL"); break;
case PERCENTAGE: cout << percent; break;
}
cout << endl;
}
int main() {
ExamInfo course1("English", 'B');
ExamInfo course2("Calculus", true);
ExamInfo course3("C++ Programming", 85);
course1.show();
course2.show();
course3.show();
return 0;
}
//运行结果:
//English: B
//Calculus: PASS
//C++ Programming: 85
2.4 枚举类
枚举类是强类型枚举
enum class 枚举类型名: 底层类型 {枚举值列表};
enum Weekday{SUN,MON,TUE,WED,THU,FRI,SAT }; //弱类型枚举,实际上是int型
enum class Type { General, Light, Medium, Heavy}; //默认int型
enum class Category { General=1, Pistol, MachineGun, Cannon}; //与弱类型一样的默认值规则
enum class Type: char { General, Light, Medium, Heavy}; //char类型
枚举类的好处:
强作用域,其作用域限制在枚举类中。避免重名干扰。
例:使用Type的枚举值General:
Type::General转换限制,枚举类对象不可以与整型隐式地互相转换。
可以指定底层类型
2.5 类的成员函数
一般
:在类中声明函数,在类外定义函数,函数名前要加上类的作用域
内联函数
:在类中定义函数
或者同一般情况,到那时在函数名外还要加上inline关键词
三、生命周期函数
构造函数,析构函数
其他还有:(拷贝构造函数,拷贝赋值函数),(移动构造函数,移动赋值函数)
3.1构造函数 constructor
在对象被创建时使用特定的值构造对象,将对象初始化为一个特定状态。
函数名与类名相同,不能定义返回值类型,也不能有return语句
处理完初始化列表之后,再执行构造函数的函数体。
类名(成员形参): 成员形参(值)... {};
可以是内联函数
可以重载
可以带默认值参数(当参数全部都有默认值的时候,注意可能跟默认构造函数产生二义性)
合成默认构造函数
注意:定义在函数体之内的内置类型和复合类型,其值均是未定义的,随机的,而对于类类型string,自动执行其构造函数,初始化为空。大概可以理解为这个默认构造函数啥事不干。
一般我们在自定义带参数构造函数之后,还需要一个无参的默认构造函数。以方便当其他人简单实用此类的时候可以简单生成对象。
#include <iostream>
#include <string>
using namespace std;
//直接定义函数
void func0(string n){cout << n;}
//简单类
class Clock
{
public:
Clock():hour(0),mineut(0),second(0){}; //默认初始化,提高稳健性,和协同性。
Clock(int mH,int mM,int mS);
void showTime();
private:
int hour, mineut, second;
}; //这个分号必须有
//类函数的定义
Clock::Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){}; //这个分号可有可无
void Clock::showTime(){
std::cout<<this->hour<<":"<<this->mineut<<":"<<this->second<<std::endl;
}
//简单数据结构
struct A{
string name;
int age;
double money;
}a={"dylan",22,222.2323}; //这个分号必须有
//主函数
int main()
{
int n = 0;
func0(a.name);
Clock mTime(12,38,22);
Clock defaultTime;
mTime.showTime();
defaultTime.showTime();
return 0;
}
委托构造函数
当重载的构造函数只是参数表和初始化列表不一样,但是算法是一致的时候,可以让代码更加优雅,保持代码一致性,避免代码重复。
针对上述的代码段,使用委托构造函数优化
//之前版本
Clock():hour(0),mineut(0),second(0){};
Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){};
//使用委托构造函数
Clock(int mH,int mM,int mS):hour(mH),mineut(mM),second(mS){};
Clock():Clock(0,0,0){};
委托的写法相当于打包转手给成熟的解决方案,非常妙。
3.2析构函数 destructor
在对象的生存期结束的时刻系统自动调用它,然后再释放此对象所属的空间。
析构函数语法:
~类名(){/*……*/};
析构函数没有返回类型和形参表
合成默认析构函数
什么也不做,跟合成默认构造函数一样,是处于语法要求必须存在的,没法禁止。
3.3拷贝构造函数
如何用一个已经存在的对象去初始化新的对象,(拷贝构造函数,拷贝赋值函数),(移动构造函数,移动赋值函数)都可以,这里先介绍拷贝构造函数。
形式:拷贝构造函数与构造函数不同在于其形参是本类对象的const 引用(节省值传递导致的对已经存在对象拷贝所产生的内存消耗)。
拷贝构造函数被调用的三种情况:
定义一个对象时,以本类另一个对象作为初始值,发生复制构造;
虚实传参:如果某函数的形参是类类型,调用函数时,将使实参对象初始化形参对象,发生复制构造;
如果函数的返回值是类类型,函数执行完成返回主调函数时,将使用return语句中的对象初始化一个临时无名对象,传递给主调函数,此时发生复制构造。
- 这种情况也可以通过移动构造避免不必要的复制
合成默认拷贝构造函数 (浅拷贝)
作用:两个对象的数据成员的一一对应复制。(所以需要传入的是本类对象的引用)
某个例子,经过浅复制,a和b的data指针都指向了相同的字符串地址,这在程序结束的析构中会发生析构两次的情况,因而产生问题。
遇到成员数据中有指针的情况,需要深拷贝。
禁止拷贝构造函数
如果不希望对象被复制:
- C++98 style:将复制构造函数声明为private且不定义它
- C++11 style:用=delete静止合成默认拷贝构造函数
四、类的组合
我们可以把类类型看成是基本类型的打包,(当然是被赋予了意义的基本类型,一同打包的还有共类的行为)
类(组合类)中的成员是另一个类(部件类)的对象。 就像是嵌套在一起了,类似继承。
提升程序的抽象性!
4.1 类组合的构造函数设计
当使用组合类的时候,我们需要特别注意部件类的初始化问题。
原则:不仅要负责对本类中的基本类型成员数据初始化,也要对对象成员初始化。
初始化次序:首先对构造函数初始化列表中列出的成员(包括基本类型成员和对象成员)进行初始化,初始化次序是成员在类体中声明的次序
。
初始化列表中未出现的成员对象,调用默认构造函数(即无形参的)初始化。详见
#include <iostream>
#include <cmath>
using namespace std;
class Point { //Point类定义
public:
Point(int xx = 0, int yy = 0) {
x = xx;
y = yy;
}
Point(Point &p);
int getX() { return x; }
int getY() { return y; }
private:
int x, y;
};
Point::Point(Point &p) { //复制构造函数的实现
x = p.x;
y = p.y;
cout << "Calling the copy constructor of Point" << endl;
}
//类的组合
class Line { //Line类的定义
public: //外部接口
Line(Point xp1, Point xp2);
Line(Line &l);
double getLen() { return len; }
private: //私有数据成员
Point p1, p2; //Point类的对象p1,p2
double len;
};
//组合类的构造函数
Line::Line(Point xp1, Point xp2) : p1(xp1), p2(xp2) {
cout << "Calling constructor of Line" << endl;
double x = static_cast<double>(p1.getX() - p2.getX());
double y = static_cast<double>(p1.getY() - p2.getY());
len = sqrt(x * x + y * y);
}
Line::Line (Line &l): p1(l.p1), p2(l.p2) {//组合类的复制构造函数
cout << "Calling the copy constructor of Line" << endl;
len = l.len;
}
//主函数
int main() {
Point myp1(1, 1), myp2(4, 5); //建立Point类的对象
Line line(myp1, myp2); //建立Line类的对象
Line line2(line); //利用复制构造函数建立一个新对象
cout << "The length of the line is: ";
cout << line.getLen() << endl;
cout << "The length of the line2 is: ";
cout << line2.getLen() << endl;
return 0;
}
这里一共会输出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;};