89.关于类的定义抽象数据类型

Posted codemagiciant

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了89.关于类的定义抽象数据类型相关的知识,希望对你有一定的参考价值。

类可以定义自己的数据类型
通过定义新的类型来反映待解决问题中的各种概念
数据抽象能帮助我们将 对象的具体实现 与 对象所能执行的操作 分离 开来

原文链接:https://blog.csdn.net/weixin_48524215/article/details/115525133

1.设计 Sales_data 类

Sales_data的接口应该包含以下操作:

●一个 isbn 成员函数,用于返回对象 (这里指某类书) 的 ISBN 编号
●一个 combine 成员函数,用于将一个 Sales_data 对象加到另一个对象上(同一种书,将其数量,价格等相加)
●一个名为 add 的函数,执行两个 Sales_data 对象的加法
●一个 read 函数,将数据从 istream (输入流)读入到Sales_data对象中
●一个 print 函数,将Sales_data对象的值输出到 ostream (输出流)

1.1使用改进的 Sales_data 类

1.从标准输入读取对象保存的数据写成 read 函数
2.将相同种类书的数据对应相加的操作(total += trans),写成一个combine函数
3.标准输出对象保存的数据写成 print 函数

Sales_data total; //对象total保存销售记录(isbn书号、销售数量、单价...)
if(read(cin, total)) //从标准输入cin读取对象total保存的数据

	Sales_data trans; //对象trans保存销售记录
	while(read(cin, trans)) //从标准输入cin读取对象trans保存的数据
	
		if(total.isbn() == trans.isbn()) //如果对象total和对象trans保存的isbn书号一致
		
			total.combine(trans); //将对象trans保存的数据与对象total保存的数据对应相加
		
        else
        
			print(cout, total) << endl; //书号不一致,输出当前对象保存的数据
			total = trans; //将此书的数据移到对象total中,以便下一次循环对比
		
	
	print(cout, total) << endl;//输出最后一条销售记录

else

	cerr << "No data?!" << endl; //未读取到数据

2.定义改进的 Sales_data 类

所有成员必须在类内部声明,成员函数定义既可在类内,也可在类外

struct Sales_data
 
//定义在类内部的函数是隐式的内联函数(编译时直接展开函数内部的东西)
//成员函数:Sales_data对象的操作
	//isbn函数返回类型为string
	std::string isbn() const return bookNo; //isbn()后的const是修饰隐式this指针的
	//combine函数返回类型为引用类型
	Sales_data& combine(const Sales_data&);
	double avg_price() const;平均价格函数,const是修饰隐式this指针的
//数据成员(对象所含有的全部属性)
	std::string bookNo; //书号
	unsigned units_sold = 0; //销售数量
	double revenue = 0.0; //销售单价
; //类定义别忘了最后加分号;
// Sales_data 的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&); //执行两个 Sales_data 对象的加法
//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
//如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
std::ostream &print(std::ostream&, const Sales_data&); //将Sales_data对象的值输出到 ostream (输出流)
std::istream &read(std::istream&, Sales_data&); //将数据从 istream (输入流)读入到Sales_data对象中

注意:

定义在类内部的函数是隐式的inline函数(参见6.5.2节, 笫214页)。

2.1定义成员函数

  尽管所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。对于Sales_data类来说,isbn函数定义在了类内,而combine和 avg_price 定义在了类外。

std: :string isbn() canst  return bookNo;) 

  和其他函数一样,成员函数体也是一个块。在此例中,块只有一条return语句,用于返回Sales_data对象的bookNo数据成员。关于isbn函数一件有意思的事情是:它是如何获得bookNo成员所依赖的对象的呢?

2.2引入this

观察对isbn成员函数的调用:

total.isbn()

  当我们调用成员函数时,实际上是在替某个对象 调用它。如果isbn指向Sales_data的成员(例如bookNo),则它隐式地指向调用该函数的对象的成员。在上面所示的调用中,当isbn返回bookNo时,实际上它隐式地返回total.bookNo。

  成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,如果调用

total.isbn()

  则编译器负责把total的 地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用或写成了如下的形式:

//伪代码,用于说明调用成员函数的实际执行过程
//类名::成员函数(传入对象地址)
Sales_data::isbn(&total);//伪代码,隐式形参this指针存着对象的地址

其中,调用Sales_data的isbn成员时传入了total的地址。

  在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看作this的隐式引用,也就是说,当isbn使用bookNo时,它隐式地使用this指向的成员,就像我们书写了this->bookNo一样。

  对于我们来说,this 形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。我们可以在成员函数体内部使用 this,因此尽管没有必要,但我们还是能把isbn定义成如下的形式:

std::string isbn() const  return this -> bookNo;   //const的作用是修饰隐式this指针的类型
//等价于return (*this).bookNo 
//this指针存着对象total的地址,对this解引用获得对象total
//等价于return total.bookNo

因为 this 的目的总是指向 “这个” 对象,所以 this是一个常量指针(所存的地址不能被改变),我们不允许改变this中保存的地址

2.3引入const成员函数

  isbn函数的另一个关键之处是紧随参数列表之后的const关键字,这里,const的作用是修改隐式this指针的类型。
  默认情况下,this的类型是指向类类型非常量版本的常量指针(指针本身是顶层const,但可以通过指针修改所属对象的内容)。例如在Sales_data 成员函数中,this的类型是Sales_data *const。尽管this是隐式的,但它仍然面要遵循初始化规则,意味着(在默认情况下)我们不能把this绑定到一个常量对象上(参见2.4.2节,第56页)。这一情况也就使得我们不能在一个常量对象上调用普通的成员函数 。

  如果isbn是一个普通函数而且this是一个普通的指针参数,则我们应该把this声明成const Sales data *const。毕竟,在isbn的函数体内不会改变this所指的对象,所以把this设置为指向常量的指针有助于提高函数的灵活性。
  然而,this是隐式的并且不会出现在参数列表中,所以在哪儿将this声明成指向常量的指针就成为我们必须面对的问题。C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数(const member function)。

//伪代码,说明隐式的this指针是如何使用的
//下面代码非法,因为不能显式地定义自己的this指针
//返回类型string,作用域运算符(::)说明成员函数isbn()被声明在Sales_data类的作用域内
std::string Sales_data::isbn(const Sales_data *const this)//非法:不能显式地定义自己的this指针
 
	return this->bookNo; //等价于(*this).bookNo等价于total.bookNo

因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容。在上例中,isbn可以读取调用它的对象的数据成员,但是不能写入新值。

注意:

常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

2.4类作用域和成员函数

  类本身就是一个作用域(参见2.6.1节,第64页)。类的成员函数的定义嵌套在类的作用域之内,因此,isbn中用到的名字bookNo其实就是定义在Sales_data内的数据成员。
  值得注意的是,即使bookNo定义在isbn之后,isbn也还是能够使用bookNo。编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

2.5在类的外部定义成员函数

  其他函数一样,当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:

double Sales_data::avg_price() const 
 
    if (units_sold) 
        return revenue/units_sold; 
    else
        return 0; 

  一旦编译器看到这个函数名,就能理解剩余的代码是位于类的作用域内的。因此,当avg_price使用revenue和units_sold时,实际上它隐式地使用了Sales_data的成员。

2.6定义一个返回 this 对象的函数

  函数combine的设计初衷类似于复合赋值运算符+=,调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象则通过显式的实参被传入函数:

//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
//如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
//lhs左侧运算对象 rhs右侧运算对象
Sales_data& Sales_data::combine(const Sales_data &rhs)

	units_sold += rhs.units_sold; //把rhs的数据成员(对象的某一属性)加到this对象的数据成员上(对象的某一属性)
	revenue += rhs.revenue;
	return *this; //this存着所指对象的地址,解引用获得对象,该函数返回类型为引用
	//返回对象的引用
	//使用this来把对象当成一个整体访问,而非直接访问对象的某个数据成员

//total为combine函数隐式形参this所属的对象,trans为右侧运算对象
total.combine(trans);//更新变量total当前的值

total的地址被绑定到隐式的 this参数上,而rhs绑定到了total上。因此,当combine执行下面的语句时,

units_sold += rhs.units_sold;//把rhs的成员添加到this对象的成员中

效果等同于求total.units_sold和trans.unit_sold 的和, 然后把结果保存到total.units_sold中。

无须使用隐式的this指针访问函数调用者的某个具体成员,而是需要把调用函数的对象当成一个整体来访问:

return *this;//返回调用该函数的对彖

其中,return语句解引用this指针以获得执行该函数的对象,换句话说,上面的这个调用返回total的引用。

3.定义类相关的非成员函数

  类的作者常常需要定义一些辅助函数,比如add、read和print等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。
  我们定义非成员函数的方式与定义其他函数一样,通常把函数的声明和定义分离开来声(参见6.1.2节,第168页)。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入个文件。

注意:

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。

3.1定义read和print函数

  下面的read和print函数与2.6.2节(第66页)中的代码作用一样,而且代码本身也非常相似:

//输入的交易信息包括ISBN、售出总数和售出价格
//IO类属于不可拷贝类型,所以只能通过引用来传递它们
//read函数从给定流(istream)中将数据读到给定的对象里
//将item绑定到传来的对象上
istream &read(istream &is, Sales_data &item)

	double price = 0;
	//给定流is读取对象的数据成员(对象的属性)
	is >> item.bookNo >> item.units_sold >> price;
	//价格*单价的结果赋值给对象的总收入
	item.revenue = price * item.units_sold;
	return is; //返回is流的引用

//print函数负责将给定对象的内容打印到给定的流中(ostream)
//将item绑定到传来的对象上
ostream &print(ostream &os, const Sales_data &item)
	//将对象的内容传到os流中
	os << item.isbn() << " " << item.units_sold << " " 
	   << item.revenue << " " << item.avg_price();
	 return os; //返回os流的引用

  除此之外,关于上面的函数还有两点是非常重要的。第一点,read和print分别接受一个各自IO类型的引用作为其参数,这是因为IO类属于不能被拷贝的类型,因此我们只能通过引用来传递它们(参见6.2.2节,第188页)。而且,因为读取和写入的操作会改变流的内容,所以两个函数接受的都是普通引用,而非对常量的引用。
  第二点,print函数不负责换行。一般来说,执行输出任务的函数应该尽量减少对格式的控制,这样可以确保由用户代码来决定是否换行。

3.2定义add函数

add函数接受两个Sales_data对象作为其参数,返回值是一个新的Sales_data,用于表示前两个对象的和:

//lhs左侧运算对象 rhs右侧运算对象
//实参传入两个对象给形参
//lhs绑定到传入的左侧实参上,rhs绑定到传入的右侧实参上
Sales_data add(const Sales_data &lhs, const Sales_data &rhs)

	Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
	sum.combine(rhs); //把rhs的数据成员加到sum中
	//lhs.combine(rhs)
	return sum; //返回两个对象的和(售出数量的和和价格总和,具体见combine函数体内的内容)
	//返回类型为非引用类型,所以返回sum的副本

4.构造函数

  每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

  构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多(参见6.4节, 第206页),不同的构造函数之间必须在参数数量或参数类型上有所区别。

  不同于其他成员函数,构造函数不能被声明成const的(参见7.1.2节, 第231页)。我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其”常量“属性。因此,构造函数在const对象的构造过程中可以向其写值。

4.1合成的默认构造函数

  我们的Sales_data类并没有定义任何构造函数,可是之前使用了Sales_data对象的程序仍然可以正确地编译和运行,举个例子,第229页的程序定义了两个对象:

Sales_data total;//保存当前求和结果的变量 
Sales_data trans;//保存下一条交易数据的变岳 

  我们没有为这些对象提供初始值,因此我们知道它们执行了默认初始化(参见2.2.1节,第40页)。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数(default constructor)。默认构造函数无须任何实参。

  默认构造函数在很多方面都有其特殊性。其中之一是,如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。

  编译器创建的构造函数又被称为合成的默认构造函数(synthesized default constructor)。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:

●如果存在类内的初始值(参见2.6.1节,第64页),用它来初始化成员。
●否则,默认初始化(参见2.2.1节,第40页)该成员。

4.2某些类不能依赖于合成的默认构造函数

  对于个普通的类来说,必须定义它自己的默认构造函数,原因有三:第一个原因也是最容易理解的一个原因就是编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。

注意:

只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

  第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。回忆我们之前介绍过的,如果定义在块中的内置类型或复合类型(比如数组和指针)的对象被默认初始化(参见2.2.1节,第40页),则它们的值将是未定义的。该准则同样适用于默认初始化的内置类型成员。因此,含有内置类型或复合类型成员的类应该在类的内部初始化这一些成员,或者定义一个自己的默认构造函数。否则,用户在创建类的对象时就可能得到未定义的值。

警告:

如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。

  第三个原因是有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。在13.1.6节(第449页)中我们将看到还有其他一些情况也会导致编译器无法生成一个正确的默认构造函数。

4.3定义Sales_data的构造函数

  对于我们的Sales_data类来说,我们将使用下面的参数定义4个不同的构造函数:

●一个istream&,从中读取一条交易信息。

●一const string&,表示ISBN编号;一个unsigned,表示售出的图书数量;以及一个double,表示图书的售出价格。

●一const string&,表示ISBN编号;编译器将赋予其他成员默认值。

●一个空参数列表(即默认构造函数),正如刚刚介绍的,既然我们已经定义了其他构造函数,那么也必须定义一个默认构造函数。

给类添加了这些成员之后,将得到

struct Sales_data

//相比之前新增的构造函数
	//希望构造函数等同于合成默认构造函数,则在构造函数的参数列表后添加 = default
	Sales_data() = default;
	//下面两个构造函数的 函数体 为空,因为构造函数的唯一目的就是为数据成员赋初值,如果没有其他任务需要执行,函数体也就为空了
	//构造函数初始值列表(冒号:和花括号函数体之间的代码)
	Sales_data(const std::string &s) : bookNo(s)  
	//类名(形参列表): 构造函数初始值列表  
	//构造函数初始值列表为新建对象的数据成员赋初值
	//没有出现在构造函数初始值列表的数据成员,将通过类内定义的初始值进行初始化
	Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p*n)  
	//如果是只接受一个string参数的构造函数,则可以写为:
	//Sales_data(const std::string &s) : bookNo(s), units_sold(0), revenue(0)   
	
	Sales_data(std::istream &);

//原来已有的其他成员
	//isbn()后的const是修饰隐式this指针的
	std::string isbn() const  return bookNo; 
	//当函数返回引用类型时,没有复制返回值,相反,返回的是对象本身
    //如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象
	Sales_data& combine(const Sales_data&);
	//avg_price() 后的const是修饰隐式this指针的
	double avg_price() const;
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
; //勿忘此处分号

4.4 =default的含义

我们从解释默认构造函数的含义开始:

Sales_data() = default;

首先请明确一点:因为该构造函数不接受任何实参,所以它是一个默认构造函数。我们定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,也需要默认的构造函数。我们希望这个函数的作用完全等同于之前使用的合成默认构造函数。

  在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。

上面的默认构造函数之所以对Sales_data有效,是因为我们为内置类型的数据成员提供了初始值。如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表(马上就会介绍)来初始化类的每个成员。

4.5构造函数初始值列表

接下来我们介绍类中定义的另外两个构造函数:

Sales_data(const std::string&s):bookNo (s)   
Sales_data(const std::string&s, unsigned n, double p): bookNo(s), units_sold(n),revenue(p*n) () 

  这两个定义中出现了新的部分,即冒号以及冒号和花括号之间的代码,其中花括号定义了(空的)函数体。我们把新出现的部分称为构造函数初始值列表(constructor initialize list),它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。

  含有三个参数的构造函数分别使用它的前两个参数初始化成员bookNo和units_sold,revenue的初始值则通过将售出图书总数和每本书单价相乘计算得到。

  只有一个string类型参数的构造函数使用这个string对象初始化bookNo,对于units_sold和revenue则没有显式地初始化。当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。在此例中,这样的成员使用类内初始值初始化,因此只接受个string参数的构造函数等价于

//与上面定义的那个构造函数效果相同
Sales_data(const std::string &s) : bookNo(s), units_sold(O), revenue(O)   

  通常情况下,构造函数使用类内初始值不失为一种好的选择,因为只要这样的初始值存在我们就能确保为成员赋予了一个正确的值。不过,如果你的编译器不支持类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。

构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。如果你不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员。

  有一点需要注意,在上面的两个构造函数中函数体都是空的。这是因为这些构造函数的唯一目的就是为数据成员赋初值,一旦没有其他任务需要执行,函数体也就为空了。

4.6在类的外部定义构造函数

  与其他几个构造函数不同,以istream为参数的构造函数需要执行一些实际的操作。在它的函数体内,调用了read函数以给数据成员赋以初值:

Sales_data::Sales_data(std::istream& is) 

    read(is, *this); //read函数的作用是从is中读取一条交易信息然后存入this对象中

  当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。因此,Sales_data::Sales_data的含义是我们定义Sales_data类的成员,它的名字是Sales_data。又因为该成员的名字和类名相同,所以它是一个构造函数。
  这个构造函数没有构造函数初始值列表,或者讲得更准确一点,它的构造函数初始值列表是空的。尽管构造函数初始值列表是空的,但是由于执行了构造函数体,所以对象的成员仍然能被初始化。

  没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话) 初始化,或者执行默认初始化。对于Sales_data来说,这意味着一旦函数开始执行,则bookNo将被初始化成空string对象,而units_sold和revenue将是0。

5.拷贝、赋值和析构

  除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等(参见6.2.1节,第187页和6.3.2节,第200页)。当我们使用了赋值运算符(参见4.4节,第129页)时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁(参见61 1节,第184页),当vector对象(或者数组)销毁时存储在其中的对象也会被销毁。

  如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。例如在7.1.1节(第229页)的书店程序中,当编译器执行如下赋值语句时,

total= trans;//处理下一本书的信息

它的行为与下面的代码相同

// Sales_data的默认赋值操作等价于:
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold; 
total.revenue = trans.revenue; 

5.1某些类不能依赖于合成的版本

  尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。举个例子,第12章将介绍C++程序是如何分配和管理动态内存的。而在13.1.4节(第447页)我们将会看到,管理动态内存的类通常不能依赖于上述操作的合成版本。
  不过值得注意的是,很多需要动态内存的类能(而且应该)使用vector对象或者string对象管理必要的存储空间。使用vector或者string的类能避免分配和释放内存带来的复杂性。

  进一步讲,如果类包含vector或者string成员,则其拷贝、赋值和销毁的合成版本能够正常工作。当我们对含有vector成员的对象执行拷贝或者赋值操作时,vector类会设法拷贝或者赋值成员中的元素。当这样的对象被销毁时,将销毁vector对象,也就是依次销毁vector中的每一个元素。这一点与string是非常类似的。
参考资料:

C++ Primer

类C++

第7章 类

类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术。封装实现了类的接口和实现额分离,封装后的类隐藏了它的实现细节。
类要想实现数据抽象和封装,需要首先定义一个抽象数据类型。

定义抽象数据类型

结构体

C语言中没有类的概念,但是有struct结构体供我们定义抽象的数据类型,但是本身不支持封装、以及类方法。

#include <iostream>
#include <cstring>
using namespace std;
struct Person

    char name[512];
    unsigned age;
;

int main(int argc, char **argv)

    Person person;
    cout << sizeof(person) << endl; // 512+4=516
    const char *name = "gaowanlu";
    strcpy(person.name, name);
    person.age = 19;
    cout << person.name << endl; // gaowanlu
    cout << person.age << endl;  // 19
    return 0;

如果结构体大小是定长时,结构体的实例内部内存是连续的,那么则会有许多的用途,比如串口协议等。

//example2.cpp
#include <iostream>
#include <cstring>
using namespace std;
struct Person

    char name[512];
    unsigned age;
;
int main(int argc, char **argv)

    Person person;
    person.age = 999;
    char *store = new char[sizeof(person)];
    memcpy(store, &person, sizeof(person));
    Person *ptr = (Person *)store;
    cout << ptr->age << endl; // 999
    delete store;
    //有点像对象的序列化是吧,在理想情况下可以通过传输介质传输内存中的二进制数据,进而达成一定的用户协议
    return 0;

方法、this

//example3.cpp
#include <iostream>
#include <string>
using namespace std;
struct Person

    string name;
    unsigned age;
    //定义在类内部的函数为隐式的inline函数
    void print() const
    
        // this是一个常量指针,不允许我们改变this中保存的地址,this永远指向对象实例本身
        std::cout << "name " << name << " age " << this->age << endl;
    
    int getAge(); //在类的内部声明
;

//外部定义类的方法
int Person::getAge()

    return this->age;


int main(int argc, char **argv)

    Person person; //定义Person类的对象实例
    person.age = 19;
    person.name = "gaowanlu";
    person.print();                  // name gaowanlu age 19
    cout << person.getAge() << endl; // 19
    return 0;

this的数据类型就是,Person*,他是一个相应类数据类型的常量指针

const成员函数

我们发现刚刚的成员函数的代码块前怎么加了const呢,有什么作用呢?

这里的const的作用是修改隐式this指针的类型,

//example4.cpp
#include <iostream>
using namespace std;
struct Person

    int age;
    void setAge(int age) const
    
        // 即const Person *this
        // this->print();//不能通过常量的指针调用函数
        // this->age = age;//不能修改对象的属性
    
    void print()
    
        cout << "person" << endl;
    
;
int main(int argc, char **argv)

    const Person person;
    const Person *ptr = &person;
    // ptr->print(); //同理类似const Person*this 不允许调用方法
    // ptr->age = 23;//不允许修改属性
    return 0;

类作用域和成员函数

类本身就是一个作用域,编译器先编译成员的声明、然后到成员函数体,所以成员函数体可以随意使用类中的其他成员而无需在意它们出现的次序。

在类的外部定义成员函数

//example5.cpp
#include <iostream>
using namespace std;
struct Person

    int age;
    void print() const;
;

void Person::print() const

    cout << this->age << endl;


int main(int argc, char **argv)

    Person person;
    person.age = 666;
    person.print(); // 666
    return 0;

返回this的函数

对于类的方法,也可以返回其对象本身的this

//example6.cpp
#include <iostream>
using namespace std;
struct Person

    int age;
    Person *add()
    
        ++(*this).age;//this的解引用
        return this;
    
;

int main(int argc, char **argv)

    Person person;
    person.age = 1;
    person.add()->add()->add(); //链式调用
    cout << person.age << endl; // 4
    return 0;

定义类相关的非成员函数

即定义普通函数,但其使用类对象做形参或者做返回值

//example7.cpp
#include <iostream>
using namespace std;
struct Person

    int age;
;

//按值传递
Person add(Person person)

    person.age++;
    return person;


//按引用传递
Person &sub(Person &person)

    person.age--;
    return person;


int main(int argc, char **argv)

    Person person;
    person.age = 0;
    Person person1 = add(person);
    cout << person.age << " " << person1.age << endl; // 0 1
    sub(person1);
    cout << person1.age << endl; // 0
    return 0;

构造函数

构造函数在创建类的对象实例时被执行
当我们没有定义构造函数时,会使用默认的构造函数,默认构造函数无需参数,也就是说只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数

//example8.cpp
#include <iostream>
using namespace std;

struct Person

    int age;
    Person() = default; //保留默认构造函数
    Person(int age)//在类内部定义地函数是隐式的inline函数
    
        this->age = age;
    
;

int main(int argc, char **argv)

    Person person1; //使用默认构造函数
    Person person2(19);
    cout << person2.age << endl; // 19
    return 0;

构造函数初始化列表

首先传入实参到构造函数、执行属性初始化列表,然后再执行构造函数体

//example9.cpp
#include <iostream>
#include <string>
using namespace std;
struct Person

    string name;
    int age;
    Person() = default;
    //初始化属性列表
    Person(string name) : name(name), age(20)
    
        cout << this->name << " " << this->age << endl;
    
;
int main(int argc, char **argv)

    Person person("gaowanlu");  // gaowanlu 20
    cout << person.age << endl; // 20
    return 0;

在类的外部定义构造函数

与普通的成员函数的操作没什么区别

//example10.cpp
#include <iostream>
#include <string>
using namespace std;
struct Person

    string name;
    int age;
    Person();
    //初始化属性列表
    Person(string name);
;

Person::Person() = default;

Person::Person(string name) : name(name),
                              age(20)

    cout << this->name << " " << this->age << endl;


int main(int argc, char **argv)

    Person person("gaowanlu");  // gaowanlu 20
    cout << person.age << endl; // 20
    return 0;

拷贝、赋值和析构

除了构造阶段,类还需要其他的控制如拷贝、赋值、销毁对象时的行为,在后面的还有详细的相关学习

//example11.cpp
#include <iostream>
#include <cstring>
using namespace std;

struct String

    char *ptr;
    String()
    
        this->ptr = new char[512];
    
    void set(const char *str)
    
        strcpy(ptr, str);
    
    ~String()
    
        if (this->ptr)
        
            cout << "delete String ptr memory\\n";
            delete this->ptr; //释放内存
        
    
;

void func()

    String str;//当栈内存被释放时 析构函数同样会被触发


int main(int argc, char **argv)

    String *str = new String();
    str->set("hello");
    cout << str->ptr << endl; // hello
    delete str;               // delete String ptr memory
    func();                   // delete String ptr memory
    return 0;

访问控制与封装

目前位置,我们并没有方法禁止某些情况不能访问到类内部的某些方法或者属性。C++语言中,我们使用访问说明符加强类的封装性。

  • private 的成员可以被类内的成员函数访问,但是不能被使用该类的代码访问到,private实现了隐藏细节暴露接口即封装的一部分

  • public 的成员在整个程序内可被访问,public成员定义类的接口

//example12.cpp
#include <iostream>
using namespace std;
struct Person

private:
    int age;

public:
    Person() = default;
    Person(int age)
    
        this->age = age;
    
    int getAge()
    
        return this->age;
    
    void setAge(int age)
    
        this->age = age;
    
;

int main(int argc, char **argv)

    Person person(19);
    person.setAge(20);
    cout << person.getAge() << endl; // 20
    // person.age;//error 访问不到
    return 0;

class与struct关键字

我们一直在使用struct也就是结构体,但是我们将其称为类,有点奇怪,其实C++支持关键词struct,而支持struct是因为要兼容C代码

二者的区别是,如果没有声明private或者public,class默认为private而struct默认为public

//example13.cpp
#include <iostream>
using namespace std;
class Dog

    int age;

public:
    void setAge(int age)
    
        this->age = age;
    
    int getAge()
    
        return this->age;
    
;
struct Cat

    int age;
;
int main(int argc, char **argv)

    Dog dog;
    Cat cat;
    // dog.age;//访问不到
    cat.age = 1;
    dog.setAge(1);
    cout << cat.age << endl;      // 1
    cout << dog.getAge() << endl; // 1
    return 0;

友员

有些函数并不是类的成员方法,但是我们仍然想要允许它访问类的私有成员,这种情况我们可以将这个函数定义为类的友元函数。

//example14.cpp
#include <iostream>
using namespace std;
class Dog

    int age;
    friend void printDog(Dog &dog);

public:
    auto setAge(int age) -> void
    
        this->age = age;
    
    auto getAge() -> int
    
        return this->age;
    
;

void printDog(Dog &dog)

    cout << dog.age << endl; //可以访问私有成员

int main(int argc, char **argv)

    Dog dog;
    dog.setAge(1);
    // dog.age;
    printDog(dog); // 1
    return 0;

一般来说、最好在类定义开始或结束前的位置集中声明友元。

封装的益处

  • 确保用户代码不会无意间破坏封装对象的状态
  • 被封装的类具体实现细节可以随时改变,指向外部提供public的接口,而无须调整接口代码

类的其他特性

类内的typedef与using

在类的内部可以使用typedef与using以至于只在类内有效,对外不隐藏细节

  • private别名不能做public方法的参数与返回值
  • 可以在public方法内使用private别名
  • 同理不能定义private别名的public属性
  • 而private则没有限制可以使用public与private别名
//example15.cpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Person

public:
    typedef std::string String;
    using StrSize = std::string::size_type;
    void setName(String name)
    
        this->name = name;
        this->name_size = name.size();
    
    StrSize name_size;
    // mList list();//error mList is private
    // void printList(mList list);//error

private:
    using mList = std::vector<int>;
    String name;
    mList list;
;
int main(int argc, char **argv)

    Person person;
    person.setName("gaowanlu");
    cout << person.name_size << endl; // 8
    // String str = "";//error: 'String' was not declared in this scope
    Person::String str = "name";
    cout << str << endl;
    return 0;

内联方法

共有三种情况

  • 隐式内联
  • 显式内联
  • 声明不指定内联、定义时指定为内联
//example16.cpp
#include <iostream>
#include <string>
using namespace std;
class Person

public:
    Person(int age, string name) : age(age), name(name)
    
    
    string getName() const //隐式内联
    
        return this->name;
    
    inline int getAge() //显式内联
    
        return this->age;
    
    void setAge(int age); //可在定义出指定内联

private:
    int age;
    string name;
;

//定义时指定为内联
inline void Person::setAge(int age)

    this->age = age;


int main(int argc, char **argv)

    Person person(18, "gaowanlu");
    cout << person.getName() << endl; // gaowanlu
    cout << person.getAge() << endl;  // 18
    person.setAge(以上是关于89.关于类的定义抽象数据类型的主要内容,如果未能解决你的问题,请参考以下文章

Java 8 - 与默认方法和抽象类的接口

类C++

类C++

关于接口和抽象类的理解

7.1 定义抽象数据类型

抽象类