想象我们经营一家书店,需要对每本书的销售数据进行统计,我们将编写一个
Sales_data
,来完成这件事,并过一次类的基础知识。
类的简介
类的本质上是一种自定义的数据类型,它基本组成为(D, S, P),D为数据对象,S为D上的关系集,P为对D的基本操作。简单来讲,就是:类=数据+操作。
类的声明以关键字struct
开始(也可以用class
,区别后面会讲。),紧跟着类名和类体(类体可为空):
1 |
|
每个类内部是一个新的作用域,所以其内部定义的名字可以和外部重复。
数据成员
类内数据的定义方法和类外相同,比如我们的销售数据要有每本书的编号bookNo,卖出的数量units_sold,收到的钱revenue。如下
1 |
|
我们可以为数据成员提供初始值(就像上面的units_sold和revenue),没有初始值的成员将被默认初始化(如bookNo将为空字符串)。
如果要在类外使用数据成员,只需在类类型后面加“.变量”:
1 |
|
成员函数
成员函数的声明在类内,定义则可以在类内或内外(如果在类内,则自动是内联的),在内外定义时要指明函数的作用域(因为类本身就是一个作用域)。比如我们给销售数据类加点东西:
1 |
|
this 指针
调用成员函数时,用类名.函数名()
的形式。当我们调用成员函数时,实际上是替某个具体对象调用它,为了使成员函数知道使哪个具体对象调用它,C++规定了一个名为this
的隐式参数,当编译时,具体对象的地址会传入this
。比如:
Sales_data total;
total.isbn();#伪代码,相当于:Sales_data::isbn(&total)
如果你的类类型是一个常量类(即具体化类时用了const),由于this指针是一个指向非常量的常量指针,所以不能绑定到常量对象上。此时可以通过在函数后面加const,使this能指向常量。比如上上面的isbn()
。推荐凡是不改变类数据的函数都加上const
最后说一句,this是隐式参数意味着我们不能定义this,但我们依然可以在函数内使用或返回this指针,比如上面isbn()
可以写成:
1 |
|
构造函数
构造函数是特殊的成员函数,其任务是初始化类对象的数据成员,如果在构造函数中没有对数据成员
初始化,则编译器会对数据成员赋默认值。构造函数有几个特点:
- 必须声明在public部分(否则无法在类外使用)
- 构造函数的名字与类名相同;
- 构造函数不能声明为const(声明成const了还怎么初始化数据成员的值?);
- 没有返回值。
当然,它也具有其他普通函数的特点,比如重载,比如默认实参等。
默认构造函数
如果不对数据成员提供初始值,则通过默认构造函数来初始化,它无须任何实参(也就没任何形参或所有形参都有默认实参)。如果我们没有定义构造函数,则编译器会隐式定义一个合成的默认构造函数,它会将按照一定规则默认初始化数据成员。
但是某些类不能用合成的默认构造函数,具体有如下几种类:
- 只要我们定义了构造函数,无论是否是默认构造函数,编译器都不会生成合成的默认构造函数;
- 数据成员含有数组和指针时,其默认初始化的值是未定义的,因此需要在类内初始化,或定义一个自己的默认构造函数;
- 如果类中包含其他类型的成员且这个成员的类型没有默认构造函数,则编译器无法默认初始化该成员。
default
如果我们定义的默认构造函数和合成的默认构造函数干的事差不多,则可以直接在构造函数的声明(或定义)的参数括号后写上= default;
值得注意的是,如果你的编译器不支持类内初始值,你就不能这样写。
构造函数初始值列表/初始化
1 |
|
像上面一样,我们可以在默认函数的括号后面加数据成员(形参)
来初始化数据成员,在这里其相当于:
1 |
|
如果要对多个数据成员初始化,它们之间用逗号隔开:
1 |
|
因为这些构造函数的唯一的作用是赋初值,所以函数体可以为空。在定义类时写成:
1 |
|
初始化与赋值的区别
- 如果数据成员是const或引用或“某种未提供默认构造函数的类类型”,则只能用初始化;
- 在底层中,实际是先初始化后赋值,所以初始化的效率比较高。
所以尽量使用初始化。
类的包含
(待定)
初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致,尽管知道顺序,但还是尽量避免使用一个数据成员初始化另一个数据成员。
复制构造函数(拷贝构造函数)
如果要通过另一个类类型来初始化,则需要通过复制构造函数将数据复制过来。复制构造函数的声明为:
struct Sales_data{
Sales_data(const Sales_data & ); //形参为必须是引用,且最好是常引用,避免误修改。
};
下面是复制构造函数的两种使用方式:
Sales_data data1;
Sales_data data2(data1);
Sales_data data3=data1;
Sales_data data4={"9-999-99999-9", 0, 0。0};//注意:这种写法只适用于聚合类(仅有数据成员而无成员函数的类)
除了主动调用复制构造函数,当函数有类类型参数或返回类类型值时,都需要隐式地调用复制构造参数(即用到临时的类类型时都要),即:
Sales_data function(Sales_data a){ //调用复制构造函数
return a;//调用复制构造函数
}
上面这段也是复制构造函数的参数必须是引用的原因。如果不是引用,则会建立临时量,而临时量本身又需要用到复制构造函数,从而造成循环。
浅复制和深复制
其实如果我们不写复制构造函数,编译器会隐式生成一个复制构造函数,但这个只能复制字面值(即类的数据成员储存的数据),即int就复制int,int* 就复制地址。这就叫浅复制。
浅复制有个问题,就是如果要复制指针,则只是复制指针所指的地址,而不分配内存空间(因为这个内存空间并不储存在类内)。如果复制得到的对象被析构了,那么原对象的指针就会指向空地址,等到原对象析构时,就会产生“释放空指针”的错误。
所以我们需要深复制:手动写一个复制构造函数,在复制构造函数里面分配新的内存空间,再复制。即
//我们另外一个类来示范
struct A{
//数据成员
int *p;
//成员函数
A(const A &a){
p = new int; //分配新的内存空间
*p=*a.p; //复制具体值,而非地址
}
};
《C++ Primer》239页:使用Vector类或string类可以避免分配和释放内存带来的复杂性。
delete(禁止复制)
如果我们不希望编译器为我们隐式生成一个复制构造函数(某些对象复制是没意义的,比如iostream,见《C++ Primer》449页),我们可以在第一次声明时,在参数的括号后面加=delete
:
1 |
|
delete
不仅适用于隐式的复制构造函数,也适用于其他隐式生成的函数(比如上面的拷贝赋值运算符的重载),析构函数除外。
另外,还有一种方法禁止复制,那就是将复制构造函数声明在private
中,并且不定义它。这样在编译的过程中就会出错。尽量不要用这种方法,而是用delete
委托构造函数
就是一个构造函数用其他构造函数来初始化。比如假如我们定义了:
1 |
|
我们可以利用这个来定义:
1 |
|
这样,当运行后面这个构造函数时,实际上是先执行第一个构造函数,再执行后面这个。
隐式类类型转换
假如我们有某个函数需要接收一个Sales_data对象,而Sales_data有一个这样的构造函数:
1 |
|
我们传递一个string对象:
1 |
|
编译器会先掉用构造函数,生成一个临时的Sales_data,由于item是一个常量引用,我们可以把临时变量传递给item。
另一种用到隐式类类型转换的情况是拷贝:
1 |
|
这种方法只能适用于只有一个变量的构造函数;并且只能适用于“一步的类类型转换”,比如下面这种就不行:
1 |
|
抑制隐式类类型转换
要是我们想禁止这种转换(比如我们想对“=”进行重载),我们可以在构造函数的声明前加explicit
来阻止(在定义处加explicit会报错):
1 |
|
注意,explicit
只能用于只有一个参数的构造函数(有多个参数怎么进行类类型转换啊~)。在这种情况下,我们依然可以显式的使用构造函数来转换:
1 |
|
析构函数
析构函数是特殊的成员函数,与构造函数相反,其任务是销毁类对象的数据成员。析构函数的基本特点是:
- 名字由波浪号+类名构成,比如
~Sales_data();
- 不接受参数,不能重载,也不返回值。因此每个类只有一个析构函数;
- 首先执行函数体,之后按初始化顺序的逆序销毁成员;
析构函数被调用的时机:
- 变量离开作用域时;
- 对象被销毁时;
- 动态分配的对象(
new
),对指向它的指针用delete
运算符时; - 临时对象,当创建它的表达式结束时;
特别的,当指向对象的引用或指针离开作用域时,并不会调用析构函数。
合成的析构函数
当一个类未定义析构函数时,编译器会为它定义一个合成的析构函数,合成的析构函数等价于:
1 |
|
注意到它的函数体为空。在此再次强调:无论函数体是否为空,析构函数首先执行函数体,之后按初始化顺序的逆序销毁成员。也就是说析构函数内的内容对后面的析构无影响。
析构指针对象
销毁一个指针时,指针所指向的对象并不会被删除。因此如果要析构指针,要手动delete
它所指的对象。我们这里的指针是内置的指针类型,C++还有一种“智能指针”,它可以自动销毁,无需手动delete
。
访问控制和封装
为了限制用户对类的访问权限,可以利用访问说明符:public
、protected
和private
(protected
后面再讲)
1 |
|
特别的,如果在访问说明符前声明数据成员或成员函数,对于struct
则默认为public
,对于class
则默认为private
。这是struct
和class
唯二的区别(另一个在继承那里)。
友元
如果希望类外的某个函数,或其他类能访问非公有成员,可以将其声明为友元,声明的位置不限(可以在public或private等):
1 |
|
最好在类定义开始或结束前的位置集中声明友元。注意,友元的声明仅指定了访问权限,并非函数声明,我们还是需要在类外声明函数。另外,友元必须和类在同一个作用域内。
如果想要将某个类的成员函数声明为友元,必须仔细组织代码结构:
1 |
|
其他成员
可变数据成员
如果我们想要修改类的某个数据成员,即使是在一个const成员函数内。可以在变量的声明前加入mutable
:
1 |
|
静态成员
声明和定义
对于一般的数据成员,每个类类型都有自己的版本;而有时候我们希望所有的类类型都共有一个数据成员(也就只有一个版本)。可以在变量的声明前加入static
:
1 |
|
静态成员不能通过特定的类类型去初始化,必须在类内声明,在类外定义(在类外定义时不能加static
)。在类内声明时,可以提供一个初始值,这样在类外定义时就不能再指定初始值。静态成员实际上类似于全局变量(因为在程序中有且只有一个),所以我们必须在类外定义,这样编译器才能为它分配空间。
使用
有两种方式使用静态成员:通过类,或通过某一类类型
1 |
|
特殊
静态成员可以是不完全类型(仅声明而未定义的类),比如:
1 |
|
而且其他成员函数可以将静态成员作为默认实参:
1 |
|