7.1 定义抽象数据类型
7.1.1 设计Sale_data类
使用改进的Sales_data类
7.1.2 定义改进的Sales_data类
定义在类内部的函数是隐式的inline函数
定义成员函数
引入this
- 成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象.当我们调用一个成员函数时,用请求该函数的对象地址初始化this.this总是指向这个对象,所以this是一个常量指针.
引入const成员函数
- 默认情况下,this的类型是指向类类型非常量版本的常量指针,例如在Sales_data成员函数中,this的类型是Sales_data* const.所以我们不能把this绑定到一个常量对象上.这一情况也就使得我们不能在一个常量对象上调用普通的成员函数
class A{
private:
int a;
void fun() {} // 这里的this为Sales const *this;
void fun1() const { // 这的this为 const Sales *const this;
fun();
}
};
- 紧跟在参数列表后面的const表示this是一个指向常量的指针.使用使用const的成员函数称为常量成员函数.因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容.
常量对象,常量对象的的指针或指针都只能调用常量成员函数
类作用域和成员函数
- 编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体.所以,成员函数体可以随意使用雷中华的其他成员而无需在意次序.
在类的外部定义成员函数
- 如果成员被声明成常量成员函数,那么它的定义也必须在参数列表后面
明确指定const
定义一个返回this对象的函数
- total.combine(trans) 那么return返回对象total的引用.
- 当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个这个运算符.
7.1.3 定义类相关的非成员函数
- 如果函数在概念上属于类但是不定义在类中,则它一般应与类声明在同一个文件中.
istream &read(istream &is,string s)
{
is >> s;
return is;
}
ostream &print(ostream &os,const string s)
{
os << s;
return os;
}
read函数从给定流中将数据读到给定的对象中,print函数则负责将给定对象的内容打印到给定的流中.
上面的函数分别接受各自的引用作为其参数,这是因为IO类属于不能被拷贝的类型,因此只能通过引用来传递他们,而且因为读取和写入都会改变他们所以使用的都是普通引用
- print函数不负责换行,一般来说输出任务的函数应该尽量减少对格式的控制.
7.1.4 构造函数
- 构造函数不能被声明成const的,当我们创建一个const对象时,直到构造函数完成初始化后,对象才能真正取得其常量属性.构造函数在const对象的构造过程中可以向其写值.
合成的默认构造函数
- 编译器创建的构造函数又被称为合成的默认构造函数.
- 合成的默认构造函数的初始化规则:
- 如果存在类内的初始值,用它来初始化(2.6.1 64)
- 否则,默认初始化.
- 合成的默认构造函数的初始化规则:
某些类不能依赖于合成的默认构造函数
如果类包含了内置类型或者符合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数
- 有时候编译器不能为某些类合成默认的构造函数.例如,如果类中包含一些其他类类型的成员的类型没有默认构造函数,那么编译器将无法初始化该成员.
7.1.5 拷贝,赋值和析构
- 当我们初始化变量以及以值的方式传递或者返回一个对象时会发生拷贝
某些类不能依赖于合成的版本
- 特别的,当类需要分配类对象之外的资源时,合成的版本常常会失效.
- 很多需要动态内存的类能使用vector对象或者string对象管理必要的存储空间.使用vector或者string的类能避免分配和释放内存带来的复杂性.即,
如果类包含vector或者string成员,则其拷贝,赋值,销毁的合成版本能够正常工作
.
7.2 访问控制与封装
- 访问说明符:public,private
使用class或struct关键字
- 两者的唯一区别是默认的访问权限不同,
7.2.1 友元
- 友元声明只能出现在类的内部.
一般来说,最好在类定义开始或者结束前的位置集中声明友元.
友元的声明
- 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明.如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元之外在专门对函数进行一次声明.但是有些编译并未强制.
- 为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一头文件中(类的外部).
7.3 类的其他特性
7.3.1 类成员再探
定义一个类型成员
- 类可以自定义某种类型在类中的别名.有类定义的类型名字和其他成员一样存在访问限制.
重载成员函数
可变数据成员
- 一个可变数据成员永远不会是const,即使它是const对象.所以,一个const成员函数可以改变一个可变成员的值.
class Person{
public:
void change() const{
age = 1; //error
name = "1221";
}
private:
mutable string name = "";
int age = 0;
};
类数据成员的初始值
当我们提供一个类内初始值时,必须一符号=或者花括号表示
返回 *this成员函数
- 返回引用的函数是左值.
从const成员函数返回*this
class Person{
public:
const Person& print() const{
cout<<name<<endl;
return *this;
}
// 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
void change(){
}
private:
mutable string name = "";
};
int main()
{
Person p;
p.print().change("xiaomi"); //error
return 0;
}
一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
基于const的重载
- 因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数.另一方面,虽然可以在非常量对象上调用常量版本和非常量版本,但显然此时非常量版本是一个更好的匹配.
7.3.3 类类型
类的声明
- 我们也可以仅仅声明类而暂时不定义类
class Screen;
- 这种声明有时被称为
前向声明
.他在声明之后定义之前是一个不完全类型
- 我们可以定义不完全类型的指针,但是无法创建不完全类型的对象.
- 不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数.
- 对于一个类来书,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明.即一个类的成员不能是该类自己.
- 然而,一旦一个类的名字出现后,它被认为是声明过的(但尚未定义),因此类允许指向他自身类型的引用或指针.
class Link{
Link *nesx;
}
7.3.4 友元再探
类之间的友元关系
友元之间不存在传递性,每个类负责控制自己的友元或友元函数
class Screen{
friend class Window;
private:
string name;
};
class Window{
public:
void clear(Screen& s){
s.name = "12";
}
};
令成员函数作为友元
- 声明为友元的函数只能在类外定义.在类内只能声明.
class Screen;
class Window{
public:
void clear(Screen& s){
s.name = "12"; //error
}
};
class Screen{
friend void Window::clear(Screen* s);
private:
string name;
};
- 正确的写法
class Screen;
class Window{
public:
void clear(Screen& s);
};
class Screen{
friend void Window::clear(Screen& s);
private:
string name;
};
void Window::clear(Screen& s){
s.name = "12"; //error
}
函数重载和友元
友元声明和作用域
7.4 类的作用域
作用域和定义在类外部的成员
- 函数的返回类型通常出现在函数名之前,因此函数定义应该在类的外部时,返回类型中使用的名字都位于类的作用域之外.这时,返回类型必须指明他是那个类的成员.
class Window_mgr{
public:
using ScreenIndex = vector<Screen>::size_type;
ScreenIndex addScreen(const Screen&);
void clear(ScreenIndex);
private:
vector<Screen> sceens{Scren(24,80,' ')};
};
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &) {
return 0;
}
因为返回类型出现在类名之前,所以事实上他是位于Window_mgr类的作用域之外的.在这种情况下,要想使用ScreenIndex作为返回类型,我们必须明确指定那个类定义了它.
7.4.1 名字查找与类的作用域
- 名字查找的过程
- 在名字所在块中寻找声明语句,只考虑在名字的使用之前出现的声明
- 如没找到,继续查找外层作用域
3.再没找到,报错
- 类的编译步骤
- 首先,编译成员的声明
- 直到类全部可见后才编译函数体 - 因为成员函数体直到整个类可见后才被处理,所以他能使用类中定义的任何名字.
用于类成员声明的名字查找
类型名要特殊处理
- 如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字.
typedef double Age;
class Person{
typedef double Age;
private:
Age age;
};
尽管重新定义类型名字是一种错误行为,但是编译器并不为此负责.一些编译器将顺利通过这样的代码
成员定义中的普通块作用域的名字查找
类作用域之后,在外围的作用域中查找
全局作用域: ::height
在文件中名字的出现处对其进行解析
7.5 构造函数再探
7.5.1 构造函数初始值列表
- 如果没有在构造函数的初始值列表中显式的初始化成员,则该成员将在构造函数体之前执行默认初始化.
构造函数的初始值有时必不可少
- 有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总是这样.
- 如下必须在初始化列表中进行初始化工作
- 成员是引用
- 成员是const
- 成员是某种类类型.且该类型没有默认构造函数
class Person{
public:
Person(int& x1,int &x2):a(x1),b(2){}
private:
int & a;
const int b;
};
成员初始化的顺序
- 构造函数初始值列表只说明用于初始化成员的值,而并不限定初始化的具体执行顺序.成员初始化的顺序与他们在类定义中的出现顺序一致.
建议:构造函数初始值顺序与成员声明的顺序保持一致.而且应该尽量避免使用一个成员去初始化另外一个成员.
默认实参和构造函数
- 如果一个构造函数为所有参数都提供了默认实参,则实际上也定义了默认构造函数.
习题
- 有些情况下,我们希望提供cin作为接受istream&参数的构造函数的默认实参
Sales_data(std::istream &is=std::cin){ is>>*this;}
7.5.2 委托构造函数
- C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的
委托构造函数
.一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说他把自己的一些职责委托给了其他函数.
- 当一个构造函数委托给另外一个构造函数,受委托的构造函数先执行.
7.5.3 默认构造函数的作用
使用默认构造函数
- 使用默认构造函数初始化的对象
Sales_data obj(); //error,声明了一个函数而非对象
Sales_data obj; //正确,ogj是一个使用默认构造函数的对象
- 不是默认构造函数是构造函数参数列表为空的函数,有些构造函数包括如干形参,而且同时为这些形参提供了默认实参,则这些构造函数也具备默认构造函数的功能.
7.5.4 隐式的类类型转换
- 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数.
- 在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则
class Person{
public:
Person(const string & na):name(na),age(22){} //参数为常量引用
Person(){}
private:
string name;
int age;
};
void test(Person p )
{}
int main()
{
string s = "小米";
test(s); //编译器用给定的string自动创建了一个Sales_data对象
return 0;
}
只允许一步类类型转换
test("小米");
这种调用时错误的.
注意:"小米"这个不是string类型这个是标注C风格字符串
类类型转换不是总有效
抑制构造函数定义的隐式转换
- 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explict加以阻止.且只能在类内声明构造函数时
使用explict关键字,在类外定义时不应重复,只能用在类的内部
.
explict构造函数只能用于直接初始化
- 用explict定义的构造函数只能用于直接初始化,不能用于拷贝形式的初始化(使用=).
class Person{
public:
explicit Person(const string & na):name(na),age(22){} //参数为常量引用
Person(){}
private:
string name;
int age;
};
int main()
{
string name = "小米";
Person p(name);
Person p = name; //error
return 0;
}
为转换显示的使用构造函数
- 尽管编译器不会将explict的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示的强制进行转换.
class Person{
public:
explicit Person(const string & na):name(na),age(22){
cout<<"___________";
} //参数为常量引用
Person(){}
private:
string name;
int age;
};
void test(Person p)
{}
int main()
{
string name = "小米";
test(static_cast<Person>(name));
return 0;
}
标准库中含有显示构造函数的类
- 接受一个单参数的const char*的string构造函数
- 接受一个容量参数的vector构造函数是exlict的
7.5.5 聚合类
-
聚合类使得用户可以直接访问其成员,其特点:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类,也没有virtual函数
-
初始值的顺序必须与声明一致.
struct Data{
int ival;
string s;
}
- 显式初始化类的对象的成员的三个缺点
- 要求类的所有成员都是public
- 将初始化每个成员的重任交给类的用户
- 添加或者删除一个成员之后,所有的初始化语句都需要更新.
7.5.6 字面值常量类
- 数据成员都是字面值类型的聚合类,是字面值常量类
- 如果一个类不是聚合类,但它符合下属要求则它也是一个字面值常量类
- 数据成员都必须是字面值类型
- 类必须至少含有一个constexpr构造函数
- 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
- 类必须使用析构函数的默认定义,该成员负责销毁类的对象
constexpr构造函数
- 尽管构造函数不能是const,但是字面值常量类的析构函数可以是constexpr函数.事实上,一个字面值常量类必须至少提供一个constexpr构造函数.
- constexpr构造函数一般来说应该是空的.我们可以通过前置关键字constexpr就可以声明一个constexpr构造函数.
- constexpr构造函数必须用于生成constexpr对象以及constexpr函数的参数或者返回类型.
7.6 类的静态成员
- 有时候类需要他的一些成员与类本身直接相关,而不是与类的各个对象相关
声明静态成员
- 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据.静态成员函数也不与任何对象绑定在一起,他们不包含this指针,作为结果,静态成员函数不能声明为const的.
因为static成员不是任何对象的组成部分,所以static成员不能被声明为const,毕竟将成员声明为const就是承诺不会修改该函数所属对象
.
使用类的静态成员
定义静态成员
- 当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句中.
- 因为静态数据成员不属于类的任何一个对象,所以他们并不是在创建类的对象时被定义的.这意味着他们不是由类的构造函数初始化的.而且,一般来说,我们不能在类的内部初始化静态成员.
- 类似于全局变量,静态数据成员定义在任何函数之外.
要想确保对象只定义一次,最好的办法是把数据成员的定义与其他内联函数的定义放在同一个文件中
静态成员的类内初始化
- 一般类的静态成员不应该在类的内部初始化,然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值类型的constexpr.初始值必须是常量表达式.
- 由于初始值必须是常量表达式,这些常用本身就是常量表达式,所以我们能在所有适合于常量表达式的地方用.例如:我们可以用一个初始化的静态数据成员指定数据成员的维度
class Account{
private:
static constexpr int period = 30;
double daily)_tb[period];
};
即使一个常量静态数据成员在类内部被初始化,通常情况下也应该在类的外部定义一下该成员
class Person{
public:
static constexpr int age = 12;
static const string name = "122";
};
constexpr int Person::age;
void test(Person p)
{}
int main()
{
cout<<Person::age;
return 0;
}
静态成员能用于某些场景,而普通成员不能
- 静态数据成员可以是不完全类型,特别的,静态数据成员的类型可以就是它所属的类类型.而非静态数据成员则受到限制,只能声明成它所属类的指针或引用
- 静态数据成员与普通成员的;另外一个区别是:我们可以使用静态数据成员作为默认形参.