c++primer第七章(类)【7.1】

2.6自定义数据结构

类以关键字struct开始,紧跟着类名和类体。类以关键字类体右侧的表示结束的花括号后必须写一个分号,因为类体后面可以紧跟变量名以表示对该类型对象的定义。

c++11规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。因此在定义个sales_data对象时,units_sold和revenue被初始化为0,bookNo被初始化为"qqq"。

struct sales_data {
	string bookNo = "qqq";
	unsigned units_sold = 0;
	double revenue = 0;
};

切记:

类内初始化只可以用“{}”,或者“=”。即花括号或者放在等号右边。不可以使用"()"

第7章

类的基本思想是数据抽象(data abstraction)和封装(encapulation)。

数据抽象(data abstraction)依赖于接口(interface)和实现(implementation)分离的变成技术。

类的接口包含用户所能执行的操作;

类的实现包含类的数据成员、负责接口实现的函数体、定义类所需的各种私有函数。

封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节。类的用户只能使用接口而无法访问具体的实现部分。

类想要实现数据抽象和封装,首先要定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责考虑类实现过程;类的使用者只需要抽象的思考类型做什么,而无需了解类型的工作细节。

7.1定义抽象数据类型

struct sales_data {
	string bookNo = "qqq";
	unsigned units_sold = 0;
	double revenue = 0;
};

sales_data不是一个抽象数据类型,它允许类用户直接访问它的数据成员。如果想把它变成一个抽象数据类型,可以定义一些操作供类用户使用,一旦定义了操作,就可以封装(隐藏)它的数据成员。

TIPS:结构体默认的访问都是public而class默认的访问机制是private。

7.1.2定义改进的sales_data类

struct sales_data {
	string isbn() const { return bookNo; }
	sales_data& combine(const sales_data&);
	double avg_price() const;
	string bookNo = "qqq";
	unsigned unit_sold{0};
	double revenue = 0;
};
//类的非成员接口函数
sales_data add(const sales_data&, const sales_data&);
ostream &print(ostream&, const sales_data&);
istream &read(istream&, sales_data&);

note:

1、成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部。

——>定义在类内部的函数都是隐式的inline函数(内部的话代码量不要太多)。

2、作为接口组成部分的非成员函数,例如add、read、print等,他们的定义和声明都在类的外部。

首先介绍isbn函数,它的参数列表是空,返回值是一个string对象。

string isbn()const { return bookNo;}

关于isbn函数有一个有意思的事情,它并没有传入参数,那它是如何获得bookNo成员所以来的对象呢?

~~~~引入this指针~~~~

让我们再观察对isbn成员函数的调用

total.isbn();

成员函数isbn其实隐式的传入了一个this指针,指向调用它的那个对象,即this = &total。对于我们来说this指针是隐式定义的。

在成员函数内部,我们可以直接调用该函数的对象的成员,而无需通过成员访问运算符来做这一点,因为this指针正指向这个对象。任何对类成员的直接访问都可以看做是this的隐式调用。

并且this指针总是指向“这个”对象。所以this是一个常量指针。我们不允许改变this保存的地址。

常量指针,指针是常量,不允许改变它的指向。

指向常量的指针,指针所指的对象是常量。

~~~~引入const成员函数~~~~

string isbn()const { return bookNo;}

const作用是修改隐式this指针的类型。默认情况下this指针类型是指向类类型的非常量版本的常量指针。

在sales_data成员函数中,this的类型是sales_data * const,意味着根据初始化准则,尽管this是隐式的,我们仍然不能把this指针绑定到一个常量对象上。所以我们不能在一个常量对象调用普通成员函数。

如果isbn是一个普通函数而this是一个普通的指针参数,则我们应该把this声明成const sales_data* const。

然而this是隐式并且不出现在参数列表。c++允许我们把const关键字放在成员函数的参数列表之后,此时紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样的const成员函数使用被称作常量成员函数(const member function)。

总结:额。。。写了这么多书上的解释。实际操作的时候我看effective c++写的:只要在成员函数内不改变变量的值,都可以直接加个const。感觉人话了点!!~!~!

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

类作用域和成员函数

struct sales_data {
	string isbn() const { return bookNo; }
	sales_data& combine(const sales_data&);
	double avg_price() const;
	string bookNo = "qqq";
	unsigned unit_sold{0};
	double revenue = 0;
};

类本身就是一个作用域。值得注意的是,即使bookNo定义在成员函数isbn()之后,isbn也可以使用bookNo。

这是因为:编译器分两步处理类,首先编译成员的声明;其次才轮到成员函数体(如果有的话)。因此成员函数体可以随意的使用类的其他成员而不用在意这些成员出现的次序。

在类的外部定义成员函数

成员函数的定义必须和它的声明匹配。即返回类型、参数列表和函数名都得与类内部的声明保持一致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。

double sales_data::avg_price() const{
    if(units_sold){
        return revenue/units_sold;
    }
    else
        return 0;
}

函数名sales_data::avg_price() 使用作用域运算符来说明如下的事实:我们定义了一个名为avg_price函数,并且该函数被声明在类sales_data的作用域内。

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

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

sales_data& sales_data::combine(const sales_data & rhs){
    units_sold += rhs.units_sold ;
    revenue += rhs.revenue ;
    return *this; 
}

total的地址呗绑定到隐式this参数上,而rhs绑定到了trans上。

该函数值得关注的是它的返回类型返回语句。一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内支付至运算符把它左侧运算对象当成左值返回,因此为了保持一致,combine函数必须返回引用类型。因为此时左侧运算对象是一个sales_data的对象,所以返回类型应该是sales_data &。

7.1.3定义类相关的非成员函数

类的操作者往往需要定义一些辅助函数:add、read、print等。尽管这些函数定义的操作从概念上来说属于类的接口组成部分,但他们实际上并不属于类本身。

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

定义read和print函数

istream &read(istream& is, sales_data&item) {
	double price = 0;
	is >> item.bookNo >> item.unit_sold  >> price;
	item.revenue = price * item.unit_sold;
	return is;
}
ostream &print(ostream& os, const sales_data&item) {
	os << item.isbn() << " " << item.unit_sold << " " << item.revenue << " " <<         
    item.avg_price();
    return os;
}

read函数从给定流中将数据读到给定的对象里,print函数则负责将给定对象的内容打印到给定的流中。

NOTE:

1、read和print分别接受一个各自IO类型的引用作为其参数。这是因为IO类属于不能被拷贝类型,因此我们只能通过引用来传递它们。而且读取和写入的操作会改变流的内容。所以函数接受的都是普通引用,而非常量的引用。

2、print函数不负责换行。一般来说执行输出任务的函数应该尽量减少对格式的控制。

定义add函数

//Right hand side, left hand side
sales_data add(const sales_data& lhs, const sales_data & rhs) {
	sales_data sum = lhs;
	sum.combine(rhs);
	return sum;
}

 

7.1.4构造函数

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

构造函数:7.5(P257),15.7(P551)、18.1.3(P689)和第13章介绍更多关于构造函数的知识。

构造函数的特点:

1、构造函数没有返回值

2、构造函数不能被声明成const ----(当类创建一个const对象时,直到构造函数完成初始化,对象才能真正获取“常量”属性。因此,构造函数在const对象构造的过程中可以向其写值)

合成默认构造函数[synthesized default constructor]

默认构造函数(default constructor):类通过一个特殊构造函数来控制默认初始化过程。默认构造函数无需实参

合成默认构造函数:我们没有定义构造函数时,编译器为我们定义的构造函数。

构造规则:

1、如果存在类内初始值,则用它来初始化成员

2、否则,默认初始化该成员。

某些类不能依赖于默认构造函数

默认构造函数只适用于非常简单的类。对于一个普通的类,必须定义它自己的默认构造函数。

原因:

1、如果一个类在某种情况下需要控制对象的初始化,那么该类很可能在所有情况下都需要控制。

(编译器发现类中不包含任何构造函数才会帮我们生成一个默认构造函数,一旦我们定义了其他构造函数,那么除非我们再定义一个默认构造函数,否则类将没有默认构造函数)

2、对于某些来来说,默认构造函数可能会执行错误的操作。

(如果定义在快中的内置类型或者符合类型(数组或指针)的对象被默认初始化,那么它们的值是未定义的)

3、有时候编译器不能为某些类合成默认的构造函数

(如果勒种包含一个其他类型的成员并且这个成员的类型没有默认构造函数)

定义sales_data的构造函数

struct sales_data {
	//新增的构造函数
	sales_data() = default;
	sales_data(const string&s):bookNo(s){}
	sales_data(const string &s , unsigned n ,double p):bookNo(s),unit_sold(n),revenue(p*n){}
	sales_data(istream& is);
	//原先的函数成员
	string isbn() const { return bookNo; }
	sales_data& combine(const sales_data&);
	double avg_price() const;
	string bookNo = "qqq";
	unsigned unit_sold{0};
	double revenue = 0;
};

=default的含义

sales_data()=default;

定义这个函数的意义在于我们不仅仅需要其他形式的构造函数,也需要默认构造函数。

构造函数初始列表

	sales_data(const string&s):bookNo(s){}
	sales_data(const string &s , unsigned n ,double p):bookNo(s),unit_sold(n),revenue(p*n){}
//构造函数的函数体都为空,这是因为这些构造函数的唯一目的就是给数据成员赋初值。一旦没有其他任务需要执行,函数体就为空了。

这两个定义出现新的部分,即冒号以及冒号和花括号之间的代码。这新出现的部分叫做构造函数初始值列表(constructor initialize list)

NOTE:通常情况下构造函数使用类内初始值不失为一种好的选择,因为这样的初始值存在可以为每一个成员赋予了一个正确的值。

BEST PRATICES:构造函数不应该情谊覆盖掉类内的初始值,除非信服的值和原值不同。如果你不能使用类内初始值,则所有构造函数都应该显示初始化每个内置类型的成员。

在类的外部定义构造函数

以istream为参数的构造函数需要执行一些实际的操作,在它的函数体内,调用read函数以给数据成员赋初值。

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

7.1.5拷贝,赋值,析构

对象在几种情况下会被拷贝:

1、初始化变量 

2、以值的方式传递

3、返回一个对象

当我们使用赋值运算符时会发生对象的赋值操作

当对象不在存在时执行销毁的操作:

1、一个局部对象在创建它的块结束时被销毁。

2、当vector对象(或数组)销毁时,存储在其中的对象也会被销毁。

如果我们不主动定义这些操作,则编译器将体我们合成它们。

如:

total = trans;
//等价于
total.bookNo = trans.bookNo;
total.units_sold = trans.units_sold ;
total.revenue = trans.revenue;

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

NOTE:特别的当类需要分配类对象之外的资源时,合成版本往往会失效。举个例子:第12章将介绍C++程序是如何分配和管理动态内存的。而13.1.4(P447)我们将指导管理动态内存的类通常不能依赖于上述操作的合成版本!!!!

BEST ADVICE:很多需要动态内存的类(而且应该)使用vector对象或者string对象管理必要的存储空间。使用vector或者string的类能避免分配和释放内存的复杂性

进一步讲,如果类包含vevctor或者string成员,则其拷贝、赋值和销毁的合成版本能够正常运行。

WARNING:在学习第13章如何自定义操作的知识之前,类中所有分配资源都应该直接以类的数据成员形式存储。

猜你喜欢

转载自blog.csdn.net/qq_34269988/article/details/84833968