文章目录
- 定义改进的Sales_data 类
- 定义成员函数
- 引入this指针
- 引入const成员函数
- 类作用域和成员函数
- 在类的外部定义成员函数
- 定义返回this的函数
- 类的静态成员
- 定义类相关的非成员函数
- 构造函数
- 合成的默认构造函数
- 某些类不能依赖合成的默认构造函数
- 定义构造函数
- 拷贝 赋值和析构
- 某些类不能过于依赖合成的版本
定义改进的Sales_data 类
对于一个简单的类,我们希望它可以完成这下面三种任务,1是有自己独特的行为,2是有自己独特的数据,3是可以和外部的函数接口,但是外部函数却只能看到接口,没有办法调用该类内部的东西,仅能通过接口来进行沟通。
struct People
{
//定义相关方法(成员函数)
void sleep() const { cout << "我在睡觉!"<<endl; }//常量成员函数
int what_age() { return age; }
//定义相关的类内参数
string name;
char sex;
int age;
};
//定义非成员函数接口:
People fun(const People&, const People&);
定义和声明成员函数的方式与普通函数差不多。成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如fun等,它们的定义和声明都在类的外部。
定义在类内部的函数是隐式的inline函数。
定义成员函数
尽管所有成员都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。
定义在类内部成员函数是自动内联的,定义在类外部的函数需要说明inline。
成员函数可以重载。
引入this指针
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如,如果调用:
people.what_age()
//上面的那个函数实际上由于this的存在,实际上是:
people.what_age(&People)
在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看作this的隐式引用,也就是说,当what_age使用age时,它隐式地使用this指向的成员,就像我们书写了this->age 一样。
对于我们来说,this形参是隐式定义的。实际上,任何自定义名为this的参数或变量的行为都是非法的。
引入const成员函数
实际上就是常量成员函数,也就是:
C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。
类成员函数声明中的 const 后缀,表明其 this 指针指向 const 型类对象,因此该 const 类对象,可以调用常量成员函数 。
一个成员函数,如果对数据成员只涉及读操作,而不进行修改操作,则尽可能声明为常量成员函数
类作用域和成员函数
好好理解一下这段代码(当时复制下来的,忘记网址了,不然加个链接)。
//情况1 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型,int,height是内层的hhdy height,int类型。
hhdy balance(){return height;}
private:
//using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况2 不报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是string类型,height是内层的hhdy height,string类型。
string balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
//情况3 报错
using hhdy=int;
string height;
class zwhy{
public:
//这里的类型hhdy是外层类型int,height是内层的hhdy height,是string类型,报错。
hhdy balance(){return height;}
private:
using hhdy = string; //此时hhdy会被重新定义为string类型
hhdy height;
} ;
在类的外部定义成员函数
像其他函数一样,当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。也就是说,返回类型、参数列表和函数名都得与类内部的声明保持一-致。如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:
void People::sleep()const
{
cout<<"我在睡觉"<<endl;
}
定义返回this的函数
实际上也就是在最后协商return *this ,但传回去的实际是进行一系列变化后的自己,这个用法将在之后的返回this的成员函数中进一步介绍。
类的静态成员
在成员声明之前加上关键字static,使之与类的所有对象关联共享。
对于静态成员函数,不能声明为常量类型,不能在静态成员函数中使用this。
定义类相关的非成员函数
类的作者常常需要定义一些辅助函数,比如fun等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。
我们定义非成员函数的方式与定义其他函数一样, 通常把函数的声明和定义分离开来。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。
一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
构造函数
每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同。和其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
不同于其他成员函数,构造函数不能被声明成const的。当我们创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。
合成的默认构造函数
我们没有为people对象提供初始值,但是他仍然进行了初始化,因此我们知道它们执行了默认初始化。类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。
如我们所见,默认构造函数在很多方面都有其特殊性。其中之一是,如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。
编译器创建的构造函数又被称为合成的默认构造函数。对于大多数类来说,这个合成的默认构造函数将按照如下规则初始化类的数据成员:
- 如果存在类内的初始值,用它来初始化成员。
- 否则,默认初始化该成员。
某些类不能依赖合成的默认构造函数
- 其一就是仅在类没有声明任何构造函数的时候,才会合成默认构造函数。
- 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。
- 有的时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。对于这样的类来说,我们必须自定义默认构造函数,否则该类将没有可用的默认构造函数。
定义构造函数
Sales_data() = default; //默认构造函数,=default在类内部,函数内联。类外部不内联。
Sales_data(const int& n):bookNo(n){}//构造函数初始值列表
Sales_data(const int& n,unsigned m,double p):bookNo(n),units_sold(m),revenue(p*m){}
Sales_data(std::istream&); //声明
//在类外部定义构造函数
Sales_data::Sales_data(std::istream& is) {
read(is, *this);
}
在其中,冒号以及冒号和花括号之间的代码,其中花括号定义了(空的)函数体。我们把新出现的部分称为构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。
拷贝 赋值和析构
除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。对象在几种情况下会被拷贝,如我们初始化变量以及以值的方式传递或返回一个对象等。当我们使用了赋值运算符时会发生对象的赋值操作。当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁,当vector对象(或者数组)销毁时存储在其中的对象也会被销毁。
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。
某些类不能过于依赖合成的版本
尽管编译器能替我们合成拷贝、赋值和销毁的操作,但是必须要清楚的一点是,对于某些类来说合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。