类的基本思想是数据抽象和封装。
数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。
class和struct的区别:class命名的类如果无public和private则统一认为为私有的,struct则是相反统一认为公有的。
struct Person
{
public:
string get_name() const {return name;}
string get_address() const {return address;}
private:
string name;
string address;
};
一、定义类
1.1引入this:
get_name函数中return name===return this->name;
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
1.2引入const成员函数:
在get_name函数紧跟参数列表之后的关键字const的作用是修改隐式this指针的类型。
默认情况下,this的类型是指向类类型非常量版本的常量指针,在Person成员函数中this为Person *const类型。
相当于: const Person a (...);
Person *const this = &a; // 底层不能赋值给顶层
this为隐式的,不能把this绑定到一个常量对象上。这一情况使得不能在一个常量对象上调用普通的成员函数。
get_name函数不会改变this所指的对象,所以把this设置为指向常量的指针有助于提高函数的灵活性。
1.3构造函数
类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数就是构造函数。
编译器创建的构造函数称为合成的默认构造函数。
构造函数类型:
Person() = default;
Person(const string&s) name(s){}
Person(const string&s, const string&n) name(s), address(n){}
Person(istream &);
1.4构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
-
如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不在再提供其他构造函数
二、访问控制与封装
使用访问说明符加强类的封装性:
· 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
· 定义private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。
2.1友元
Person的数据成员是private,则非成员函数则不能调用它们,也就无法正常编译,尽管这几个函数是类的接口的一部分,但他们不是类的成员函数。
可以使用友元friend令其他类或者函数可以访问它的非公有成员。
2.2友元的声明
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。
三、类的其他特性
继续介绍类的娶她特性:类型成员、类的成员的类内初始值、可变数据成员、内联成员函数、从成员函数返回*this、关于如何定义并使用类类型及友元类的更多知识。
如果在类中已经提供了一个构造函数,则编译器将不会自动生成默认的构造函数。如果需要默认构造函数,必须把它显示地声明出来。
3.1可变数据成员
当需要修改类的某个数据成员时,即使在一个const成员函数内。可以通过在变量声明中加入mutable关键字做到这一点。
一个可变数据成员永远不会是const:
mutable size_t access_ctr;
void Screen::some_member()const{ ++access_ctr;}
3.2类数据成员的初始值
vector<Screen> screens{Screen(24,80,' ')}; // 当初始化类类型的成员时,需要为构造函数传递一个符合成员类型的实参 .
3.3友元再探
友元关系不存在传递性。
友元声明和作用域:类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,隐式地假定该名字在当前作用域是可见的。
3.4构造函数再探
构造函数初始值列表
当定义变量时习惯于立即对齐进行初始化,而非先定义、再赋值:
string foo = "hello"; // 定义并初始化
string bar; // 默认初始化成空string对象
bar = "hello:; // 为bar赋一个新值
就对象的数据成员而言,初始化和赋值也有类似的区别。这段代码执行了赋值操作。
Sales_data::Sales_data(const string&s, unsigned cnt, double price)
{
bookNo = s;
units_sold = cnt;
revenue = cnt * price;
}
默认实参和构造函数
class Sales_data
{
public:
// 定义默认构造函数,令其与只接受一个string实参的构造函数相同
Sales_data(string s = " "): bookNo(s){}
Sales_data(string s, unsigned cnt, double rev)
:bookNo(s), units_sold(cnt), revenue(rev*cnt){}
Sales_data(istream &is) {read(is, *this);}
};
3.5委托构造函数
委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些职责委托给了其他构造函数。
class Sales_data
{
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(string s, unsigned cnt, double rev)
:bookNo(s), units_sold(cnt), revenue(rev*cnt){}
// 其余构造函数全都委托给另一个构造函数
Sales_data():Sales_data(" ", 0, 0) {}
Sales_data(string s):Sales_data(s, 0, 0) {}
Sales_data(istream &is) :Sales_data(){read(is, *this);}
};
在这个Sales_data类中,除了一个构造函数外其他的都委托了它们的工作。当一个构造函数给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依此执行。
3.6默认构造函数的作用
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化在以下情况下发生:
· 当我们再块作用域内不使用任何初始值定义一个非静态变量或数值时。
· 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
· 当类类型的成员没有在构造初始值列表中显示地初始化时。
值初始化在以下情况发生情况:
· 数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
· 当我们不使用初始值定义一个局部静态变量时。
· 当我们通过书写如T()的表达式显示地请求初始化时,其中T是类型名。
class NoDefault{
public:
NoDefault(const string&); // 还有其他成员,但是没有其他构造函数了
};
struct A{
Nodefaulit my_men; // 默认情况下my_mem是public的
};
A a; // 错误:不能为A合成构造函数
struct B{
B() {} // 错误:b_member没有初始值
NoDefault b_member;
};
3.7隐式的类类型转换
构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时把这种构造函数称作转换构造函数。
在Sales_data类中,接受string的构造函数和接受istream的构造函数分别定义了各自的隐式转换的规则:
string null_book = "9-999-99999-9"; // 构造了一个临时的Sales_data对象
// 该对象的units_sold和revenue等于0,bookNo=null_book
item.combine(null_book); // 编译器给null_book自动创建了一个Sales_data对象
只允许一步类类型转换
编译器只会自动执行一步类型转换:
item.combine("9-999-99999-9");
// 错误:首先需要转换为string类型
// 其次把这个临时变量转换为Sale_data
如果想完成上述调用,可以显示把字符串转换为string或Sales_data对象
抑制构造函数定义的隐式转换:
可以将构造函数声明为explicit加以阻止。
并且explicit构造函数只能用于直接初始化:发生隐式转换的一种情况是当执行拷贝形式的初始化时。此时我们只能使用直接初始化而不能使用explicit函数:
Sales_data item1(null_book); // 正确:直接初始化
Sales_data item2 = null_book; // 错误:不能将explicit构造函数用于拷贝形式的初始。
为转换显式地使用构造函数:
尽管编译器不会将explicit的构造函数用于隐式转换过程,但是可以使用下面的构造函数显式地强制进行转换
item.combine(Sales)data(null_book));
// 正确:实参使一个显式构造的Sales_data对象
item.combine(static_cast<Sales_data>(cin));
// 正确:static_cast 可以使用explicit的构造函数
3.8聚合类
聚合类使得用户可以直接 访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,说他是聚合的:
· 所有成员都是public的;
· 没有定义任何构造函数;
· 没有类内初始值;
· 没有基类,也没有virtual函数。
struct Data{
int ival;
string s;
};
可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:
// vall.ival = 0; val.s = string("na")
Data vall = {0, "na"};
初始值的顺序必须与声明的顺序一致。
3.9字面值常量类
提到过constexpr函数的参数和返回值必须是字面值类型。除了算术类型、引用和指针外,某些类也是字面值类型。和其他类不同,字面值类型的类可能含有constexpr函数成员。它们是隐式const的。
数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合来,但它符合以下要求,则它也是一个字面值常量类:
· 数据成员都必须是字面值类型;
· 类必须至少含有一个constexpr构造函数;
· 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果
成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数;
· 类必须使用析构函数的默认定义,该成员负责销毁类的对象。
constexpr构造函数不能是const的,但可以是constexpr函数:
class Debug{
public:
constexpr Debug(bool b = true): hw(b), io(b), other(b) {}
constexpr Debug(bool h, bool i, bool o): hw(h), io(i) other(o) {}
constexpr bool any() {return hw||io||other;}
void set_io(bool b) {io = b;}
void set_hw(bool b) {hw = b;}
void set_ohter(bool b) {hw = b;}
private:
bool hw;
bool io;
bool other;
};
constexpr构造函数必须初始化所有数据成员,初始化或者使用constexpr构造函数,或者是一条常量表达式。
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:
const Debug io_sub(false, true, false); // 调试IO
if (io_sub.any()) // 等价于if(true)
cerr << "xxx" <<endl;
const Debug prob(false); // 无调试
if (prob.any())
cerr << "xxx" <<endl; // 等价于if(true)
四、类的静态成员
有的时候类需要一些成员与类本身直接相关,而不是与类的各个对象保持关联——静态成员
声明静态成员:
// 表示银行的账户记录的类
class Account{
public:
void calculate() {amount += amount * interestRate;}
static double rate() {return interestRate;}
static void rate(double);
private:
string owner;
static double interestRate;
static double initRate();
静态成员函数不与任何对象绑定到一起,不包含this指针。作为结果,静态成员函数不能声明为const的,而且也不能在static函数体内使用this指针。这一限制即适用于this的显式使用,也对调用非静态成员的隐式使用有效。
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象被定义的。这意味着它们不是由类的构造函数初始化的。
静态成员的类内初始化:
在静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。
class Account{
public:
static double rate() {return interestRate;}
static void rate(double);
private:
static constexpr int period = 30; // period是没常量表达式
double daily_tbl[priod];
};
静态成员能用于某些场景,而普通成员不能在某些非静态数据成员可能非法的场景,静态成员却可以正常地使用。举个例子,静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用:
class Bar{
public:
// ...
private:
static Bar mem1; // 正确:静态成员可以是不完全类型
Bar *men2; // 正确:指针成员可以不完全类型
Bar men3; // 错误:数据成员必须是完全类型
};
静态成员和普通成员的另一个区别是我们可以使用静态成员作为默认实参:
class Screen{
public:
// bkground表示一个在类中稍后定义的静态成员
Screen& clear(char = bkground);
private:
static const char bkground;
};
非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终引发错误。