Fork me on GitHub

C++Primer类

C++ Primer类

构造函数

1.定义:

​ 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。

2.特点:

​ 名字与类名相同、

​ 没有返回类型、

​ 有一个可能为空的参数列表和函数体、

​ 不能被声明为const的

默认构造函数:

​ 类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数,默认构造函数无需任何实参。

​ 如果我们的类没有显示的定义一个默认构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。

Note

​ 1. 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

​ 2. 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数(否则得到未定义的值)。

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

Example

struct Sales_data{
    Sales_data() = default; //如果我们需要默认的行为,可以在参数列表后面加上= default来要求编译器生成构造函数
    Sales_data(const std::string &s):
    	bookNo(s) {}
    Sales_data(const std::string &s, unsigned n, double p):
    	bookNo(s), units_sold(n), revenue(p*n){}
    Sales_data(std::istream&);
    std::string isbn() const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    double avg_price() const;
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

构造函数初始值列表

​ 如果你的编译器不支持类内初始值,那么你的默认构造函数就应该使用构造函数初始值列表来初始化类的每个成员。

Sales_data(const std::string &s):bookNo(s), units_sold(0), revenue(0) {} 
//当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。(编译器支持类内初始值的情况下)
Sales_data(const std::string &s):bookNo(s) {}  //与前者相同

Sales_data(const std::string &s, unsigned n, double p):
	bookNo(s), units_sold(n), revenue(p*n) {}

在类的外部定义构造函数

​ 当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。Sales_data::Sales_data的含义是我们定义Sales_data类的成员,它的名字是Sales_data,因为构造函数没有返回类型,并且该成员名字与类名相同,所以它是一个构造函数。

//类名::成员函数名
Sales_data::Sales_data(std::istream &s)
{
	read(is, *this)
}

访问控制与封装

C++中,我们使用访问说明符加强类的封装性。

  1. 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。

  2. 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

    class Sales_data{
    //构造函数和部分成员函数    
    public:
    	Sales_data() = default;
    	Sales_data(const std::string &s, unsigned n, double p):
    		bookNo(s), units_sold(n), revenue(p*n) {}
    	Sales_data(const std::string &s): bookNo(s) {}
    	Sales_data(std::istream&);
    	std::string isbn() const {return bookNo;}
    	Sales_data &combine(const Sales_data&);
    //数据成员和作为实现部分的函数    
    private:
    	double avg_price() const
    		{return units_sold ? revenue/units_sold : 0;}
    	std::string bookNo;
    	unsigned units_sold = 0;
    	double revenue = 0.0;
    };
    

使用class或struct关键字

​ 唯一的区别是默认的访问权限不一样。使用struct关键字,则定义在第一个访问说明符之前的成员是public的;使用class关键字,则定义在第一个访问说明符之前的成员是private的。

友元

​ 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为友元,只需要增加一条friend关键字开始的函数声明语句即可。

class Sales_data{
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&)

public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p):
		bookNo(s), units_sold(n), revenue(p*n) {}
	Sales_data(const std::string &s): bookNo(s) {}
	Sales_data(std::istream&);
	std::string isbn() const {return bookNo;}
	Sales_data &combine(const Sales_data&);

private:
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

//Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&)

友元声明只能出现在类定义的内部,在类内出现的具体位置不限,最好在类定义开始或结束前的位置声明友元。

友元声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果需要调用友元函数,必须在友元声明之外专门对函数进行声明

封装的益处

  1. 确保用户代码不会无意间破坏封装对象的状态。
  2. 被封装的类的具体实现细节可以随时改变,无须调整用户级别的代码。

类成员再探

定义一个类型成员

除了定义数据和函数成员外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private中的一种。

class Screen{
public:
	typedef std::string::size_type pos;
    //using pos = std::string::size_type;
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

Screen类的成员函数

class Screen{
public:
	typedef std::string::size_type pos;
	Screen() = default;
    //为cursor隐式地使用了类内初始值,如果类中不存在cursor的类内初始值,就需要显示初始化cursor了
	Screen(pos ht, pos wd, char c): height(ht), width(wd),
		contents(ht*wd,c) {}
    
    //定义在类内部的成员函数是自动inline的
	char get() const                       //读取光标处的字符串,定义
		{return contents[cursor]}          //隐式内联(定义在类内部的成员函数自动inline)
	inline char get(pos ht, pos wd) const; //显示内联,声明
    
	Screen &move(pos r, pos c);            //声明,在之后被设为内联(如下)
    //为什么没有隐式内联? 因为不是定义而是声明啊。
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	std::string contents;
};

我们在类的内部把inline作为声明的一部分显示地声明成员函数;同样也可以在类的外部用inline关键字修饰函数的定义

inline   //在类的定义处指定inline
Screen &Screen::move(pos r,pos c)
{
	pos row = r*width;  //计算行的位置
	cursor = row + c;   //在行内将光标移动到指定的列
	return *this;       //左值形式返回对象
}

//在类的内部已经声明inline
char Screen::get(pos r, pos c) const 
{
	pos row = r*width;  
	return contents[row+c];
}

最好只在类外部定义的地方声明inline。

重载成员函数

和非成员函数一样,成员函数也可以被重载,只要函数在参数或类型上有所区别就行。

可见数据成员

有时会发生这样的情况,我们希望修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到这一点。

class Screen{
public:
	void some_member() const;
private:
	mutable size_t access_ctr;  //即使在一个const对象内也能被修改
};

void Screen::some_member() const
{
	++access_ctr;   //记录成员函数被调用的次数
}

类数据成员的初始值

class Window_mgr {
private:
    //Window_mgr追踪的Screen
    //默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
	std::vector<screen> screens{Screen(24, 80, ' ')};
    //使用=/{}来提供类内初始值
}

返回*this的成员函数

class Screen{
public:
	Screen &set(char);
	Screen &set(pos, pos, char);
};

inline Screen &Screen::set(char c)
{
	contents[cursor] = c;
	return *this;
}

inline Screen &Screen::set(pos r, pos col, char ch)
{
	contents[r*width + col] = ch;
	return *this;
}
myScreen.move(4,0).set('#')

从const成员函数返回*this

一个const成员函数如果以引用的方式返回*this,那么它的返回类型将是常量引用,我们不能把它嵌入到一组动作的序列中去。

Screen myscreen;
//如果display返回常量引用,调用set会发生错误
myScreen.display(cout).set('*');

基于const的重载

通过区分成员函数是否是const的,我们可以对其进行重载,其原因与我们之前根据指针参数是否指向const而重载函数的原因差不多。这是因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数。另一方面,虽然可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是更好的匹配。

class Screen{
public:
    //根据对象是否是const重载了display函数
	Screen &display(std::ostream &os)    //非常量版本,返回一个非常量引用(指向非常量的指针->指向常量的指针->非常量引用???)
		{do_display(os);return *this;}
	const Screen &display(std::ostream &os) const   //常量版本,返回常量引用
		{do_display(os); return *this;}
private:
	void do_display(std::ostream &os) const {os<<contents;}  //指向常量则不变;指向非常量则隐式地转换成指向常量的指针
};

第一步:display执行,调用do_display

​ 当一个成员调用另一个成员时,this指针在其中隐式的传递。当display调用do_display时,它的this指针隐式的传递给do_display;而当display的非常量版本调用do_display时,它的this指针隐式地从指向非常量的指针转换成指向常量的指针

第二步:do_display完成,display解引用

​ 当do_display完成后,display函数各自返回解引用this所得的对象。在非常量版本中,this指向一个非常量对象,因此display返回一个普通的非常量引用;而const成员则返回一个常量引用

当我们在某个对象上调用display时,该对象是否是const决定了应该调用display的哪个版本。

Screen myScreen(5,3);
const Screen blank(5,3);

myScreen.set('#').display(cout);  //调用非常量版本
blank.display(cout);              //调用常量版本

建议:对公共代码使用私有功能函数

类类型

每个类定义了唯一的类型,对两个类来说,即使它们的成员列表完全一致,它们也是不同的类型

struct First{
	int memi;
	int getMem();
};

struct Second{
	int memi;
	int getMem();
};

First obj1;
Second obj2 = obj1;   //错误:两者类型不同

我们可以把类名作为类型的名字使用,直接指向类类型;也可以把类名跟在关键字calss或struct后面。

Sales_data item1;
class Sales_data item1; //等价前者

类的声明

我们可以仅声明类而暂时不定义它,class Screen。这种声明有时被称为前向声明,它向程序中引入了名字Screen并指明Screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型,即此时我们知道Screen是一个类类型,但是不清楚它的成员。

类必须首先被定义,然后才能引用或者指针访问其成员。然而,一旦一个类的名字出现之后,就被认为是声明过了,因此类允许包含指向它自身类型的引用或指针

class Link_screen{
	Screen window;
	Link_screen *next;
	Link_screen *prev;
};

友元再探

类之间的友元关系

class Screen{
    //Window_mgr的成员可以访问Screen类的私有部分
	friend class Window_mgr;
    //Screen类的剩余部分
}

如果一个类指定了友元类,那么友元类的成员函数可以访问此类包括非公有成员在内的所有成员

class Window_mgr{
public:
    //窗口中每个屏幕的编号
	using ScreenIndex = std::vector<Screen>::size_type;
    //按照编号将指定的Screen重置为空白
	void clear(ScreenIndex);
private:
	std::vector<Screen> screens{Screen(24, 80, ' ')};
};

//成员函数
void Window_mgr::clear(ScreenIndex i)
{
    //s是一个Screen的引用,指向我们想清空的屏幕
	Screen &s = screens[i];
    //将选定的Screen重置为空白
	s.contents = string(s.height * s.width, ' ')
}

必须要注意的是友元关系不存在传递性,即如果Window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权。

Note: 每个类负责控制自己的友元类或函数。

令成员函数作为友元

Screen还可以只为clear提供访问权限,当把一个成员函数声明为友元时,必须明确指出成员函数属于哪个类

class Screen{
	//Window_mgr::clear必须在Screen类之前被声明
	friend void Window_mgr::clear(ScreenIndex);
	//Screen类的剩余部分
}

设计顺序必须满足依赖关系:

  1. 首先定义Window_mgr类,声明clear函数,但不定义。在clear使用Screen的成员之前必须先声明Screen
  2. 定义Screen,包括对于clear的友元声明
  3. 定义clear,此时才可以使用Screen的成员

函数重载和友元

重载函数尽管名字相同,但仍是不同的函数。如果一个类想把一组重载函数声明成它的友元,它需要这组函数中的每一个分别声明

extern std::ostream& storeOn(std::ostream &, Screen &);//extern关键字可置于变量或者函数前,以标示变量或者函数的定义在别的文件中
extern BitMap& storeOn(BitMap &, Screen &);
    
class Screen{
    //storeOn的ostream版本能访问Screen对象的私有部分
	friend std::ostream& storeOn(std::ostream &, Screen &);  
};

友元声明和作用域

类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域是可见的。然而,友元本身不一定真的声明在当前作用域中。即使仅仅是用声明友元的类的成员调用该友元函数,它也必须是声明过的。

struct X{
	friend void f() {/*友元函数可以定义在类的内部*/}
	X() {f();}  //错误,友元函数f还没有被声明  ps:这是什么格式?
	void g();
	void h();
};

void X::g() {return f();} //错误:f还没有被声明
void f();  //声明定义在X中的函数f
void X::h() {return f();} //现在f的声明在作用域中了

类的作用域

每个类都会定义自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员运算符来访问。对于类类型成员,则使用作用域运算符访问。

Screen::pos ht=24, wd=80;
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get();   //访问scr对象的get成员
c = p ->get();  //访问p所指对象的get成员

作用域和定义在类外部的成员

一个类就是一个作用域的事实能够很好地解释为什么当我们在类的外部定义成员函数时必须同时提供类名和函数名。在类的外部,成员的名字被隐藏了。

一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体,结果就是我们可以直接使用类的其他成员而无需再次授权了。

void Window_mgr::clear(ScreenIndex i)
{
	Screen &s = screens[i];
	s.contents = string(s.height * s.width, ' ');
}

另一方面,函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。

class Window_mgr{
public:
    //向窗口添加一个Screen,返回它的编号
	ScreenIndex addScreen(const Screen&);
    //其他成员与之前版本一致
};
//首先处理返回类型,之后才进入Window_mgr的作用域
Window_mgr::ScreenIndex    //作用域之外
Window mgr::addScreen(const Screen &s)
{
	screens.push_back(s);  //作用域之内
	return screens.size() - 1;
}

名字查找与类的作用域

名字查找的过程直截了当。

  1. 首先,在名字所在的块中寻找声明语句,只考虑在名字的使用之前出现的声明。
  2. 如果没有找到,继续查找外层作用域。
  3. 如果最终没有找到匹配的声明,报错。

对于定义在类内部的成员函数,解析其中名字的方式与上述规则有所区别,类的定义分两步走。

  1. 首先,编译成员的声明
  2. 直到类全部可见才编译函数体(编译器处理完类中的全部声明后才会处理成员函数的定义

按照这种方式处理类可以简化类代码的组织方式。因为成员函数体直到整个类可见后才会被处理,所以它能使用类中定义的任何名字;相反,如果函数的定义和成员的声明被同时处理,那么我们将不得不在成员函数中只使用那些已经出现的名字。

用于类成员声明的名字查找

这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中的名字,都必须在使用前确保可见。如果一个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。

typedef double Money;
string bal;

class Account{
public:
   //编译器只考虑Account中在使用Money之前出现的声明,因为没有找到匹配的成员,所以编译器会接着到Account的外层作用域插值,最终匹配到typedef语句
	Money balance() {return bal;} 
   //函数体在整个类可见后才被处理,return返回名为bal的数据成员,而非外层作用域的string对象
private:
	Money bal;
	//...
};

类型名要特殊处理

在类中,如果成员使用了外层作用域的名字,而且该名字代表一种类型,则类不能在之后重新定义该名字

typedef double Money; //double类

class Account{
public:
	Money balance() {return bal;} //使用外层作用域中的Money
private:
	typedef double Money;  //错误,不能重新定义Money,哪怕类型一致也不行
	Money bal;
	//...
};

Tips类型名的定义通常出现在类的开始处,能确保所有使用该类型的成员都出现在类名的定义之后。

成员定义中普通块作用域的名字查找

成员函数中使用的名字按照如下方式解析:

  1. 首先,在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才能被考虑
  2. 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑
  3. 如果类内也找不到,那么在成员函数定义之前的作用域内继续查找
//不建议使为参数和成员使用同样的名字
int height;
class Screen{
public:
	typedef std::string::size_type pos;
	void dummy_fcn(pos height){  //指的是参数声明
		cursor = width * height;
	}
private:
	pos cursor = 0;
	pos height = 0, width = 0; //height隐藏了同名成员
};
//不建议:成员函数中的名字不应该隐藏同名成员
void Screen::dummy_fcn(pos height){
	cursor = width * this->height;
    //另一种写法
	cursor = width * Screen::height;
}

//建议的写法:不要把成员名字作为参数或其他局部变量使用,给参数起个其他名字
void Screen::dummy_fcn(pos ht){
    	cursor = width * height; //成员height
}

类作用域之后,在外围的作用域查找

如果编译器在函数和类的作用域中都没有找到名字,它将接着在外围的作用域中查找。上例中height定义在类作用域之外,然而却被height成员隐藏了,因此我们需要显示地通过作用域运算符来请求外层作用域中的名字。

void Screen::dummy_fcn(pos height){
	cursor = width * ::height;  //全局height
}

在文件名字的出现处对其进行解析

成员定义在类的外部时,名字查找的第三步不仅需要考虑类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明

int height;
class Screen{
public:
	typedef std::string::size_type pos;
	void setHeight(pos);
	pos height = 0;	 //隐藏了外层作用域中的height
};

Screen::pos verify(Screen::pos);
void Screen::setHeight(pos var){
    //var:参数
    //height:类的成员
    //verify:全局函数
	height = verify(var);
}

全局函数verify的声明在Screen类的定义之前是不可见的。然而名字查找第三步包括了成员函数出现之前的全局作用域,verify的声明位于成员函数定义之前,可以正常使用。

构造函数再探

构造函数初始值列表

我们定义变量时习惯于立即对其进行初始化,而非先定义、再赋值。就对象的数据成员而言,初始化和赋值也有类似区别,如果没有在构造函数的初始值列表中就显式地初始化成员,那么该成员将在构造函数体之前执行默认初始化。

Sales_data::Sales_data(const string &s, unsigned cnt, double price)
{
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}

构造函数的初始值有时必不可少

如果成员是const或者引用,必须将其初始化。类似的,当成员属于某种类类型且该类没有定义默认构造函数,也必须将这个成员初始化。

class ConstRef{
public:
	ConstRef(int ii);      //构造函数的声明
private:
	int i;
	const int i;
	int &ri;
};

//错误:ci和ri必须被初始化
ConstRef::ConstRef(int ii)  //构造函数的定义
{   //赋值
    i = ii;
    ci = ii; //错误:不能给const赋值
    ri = i;  //错误:ri没有被初始化
}

我们初始化const或者引用数据成员的唯一机会就是通过构造函数初始值,因此该构造函数的正确形式如下。

//显示地初始化引用和const成员
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i) {}

成员初始化的顺序

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序

成员初始化的顺序与它们在类定义中出现顺序一致。一般来说初始化的顺序没有特别要求,不过如果一个成员是用另一个成员来初始化的,那么这两个成员的初始

化顺序就很重要了。

class X{
	int i;
	int j;
public:
	X(int val):j(val), i(j) {} //构造函数,由于i在j之前被初始化,所以未定义,试图使用未定义的j初始化i
    X(int val):i(val), j(val) {} //正确
};

Tips:最好令构造函数初始值的顺序成员声明的顺序一致,如果可能的话,尽量避免使用某些成员初始化其他成员

默认实参和构造函数

Sales_data默认构造函数的行为与只接受一个string实参的构造函数差不多。

唯一区别是接受string实参的构造函数使用这个实参初始化bookNo,而默认构造函数隐式地使用string的默认构造函数初始化bookNo.

class Sales_data{
public:
    //定义默认构造函数,使其与只接受一个string实参的构造函数功能相同
	Sales_data(std::string s=" "):bookNo(s) {}
    //其他构造函数与之前一致
	Sales_data(std::string s,unsigned cnt, double rev):
		bookNo(s), units_sold(cnt), revenue(rev*cnt) {}
	Sales_data(std::istream &is) {read(is, *this);}
    //其他成员与之前版本一致
};

因为我们不提供实参也能调用上述构造函数,所以该构造函数实际上为我们的类提供了默认构造函数

委托构造函数

C++11使得我们可以定义所谓的委托构造函数,委托构造函数把它自己的一些职责委托给了其他构造函数,即使用它所属类的其他构造函数执行它自己的初始化过程。

class Sales_data{
public:
    //非委托构造函数使用对应的实参初始化成员
	Sales_data(std::string s, unsigned cnt, double price):
		bookNo(s), units_sold(cnt), revenue(cnt*price) {}
    //其余构造函数全都委托给另一个构造函数
	Sales_data():Sales_data(" ", 0, 0) {}
	Sales_data(std::string s):Sales_data(s,0,0) {}
	Sales_data(std::istream &is): Sales_data()
		{read(is, *this)}
    //其他成员与之前版本一致
};

当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行

默认构造函数的作用

当对象被默认初始化值初始化时自动执行默认构造函数。类必须包含一个默认构造函数以便在上述情况中使用。

使用默认构造函数

如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空的括号对。

Sales_data obj();  //错误:声明了一个函数
Sales_data obj2;   //声明了一个对象

隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时候我们把这种构造函数称为转换构造函数

即能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。

只允许一步类类型转换

类类型的转换并不总是有效(临时量会被丢弃)

抑制构造函数定义的隐式转换

C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的。

class Sales_data{
public:
	Sales_data() = default;
	Sales_data(const std::string &s, unsigned n, double p):
		bookNo(s), units_sold(n), revenue(p*n) {}
	explicit Sales_data(const std::string &s): bookNo(s) {} //错误:string构造函数是explicit的
	explicit Sales_data(std::istream&);                     //错误:istream构造函数是explicit的
};

关键字explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无须使用explicit。

//错误:explicit关键字只能出现在类内的构造函数声明处
explicit Sales_data::Sales_data(istream& is)
{
	read(is, *this);
}

只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应该重复。

explicit构造函数只能用于直接初始化

当我们直接执行拷贝形式=的初始化时,会发生隐式转换;此时,我们只能使用直接初始化()而不能使用explicit构造函数。

Sales_data item1(null_book);  //正确,直接初始化
Sales_data item2 = null_book; //错误:不能将explicit构造函数用于拷贝初始化

为转换显示地使用构造函数

尽管编译器不会将explicit的构造函数用于隐式转换过程,但是我们可以使用这样的构造函数显示地强制进行转换

//正确:实参是一个显示构造的Sales_data对象
item1.combine(Sales_data(null_book));
//正确:static_cast可以使用explicit的构造函数
item1.combine(static_cast<Sales_data>(cin));

标准库中含有显示构造函数的类

我们用过的一些标准库的类中含有单参数的构造函数

  1. 接受一个单参数的const char*的string构造函数,不是explicit
  2. 接受一个容量参数的vector构造函数,是explicit

聚合类

聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足以下条件可以说类是聚合的:

  1. 所有成员public的

  2. 没有定义任何构造函数

  3. 没有类内初始值

  4. 没有基类,没有virtual函数

    struct Data{
    	int ival;
    	string s;
    };
    

    我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。

    //vall.ival = 0;vall.s = string("Anna")
    Data val1 = {0, "Anna"};
    Data val2 = {"Anna", 1024};  //错误:初始值顺序不一致
    

    初始值的顺序必须与声明的顺序一致。如果初始值列表的元素个数小于类的成员数量,则靠后的成员被值初始化

    显示地初始化类的对象的成员有三个缺点:

    1. 要求类的成员都是public
    2. 初始化过程冗长且容易出错
    3. 添加或者删除成员后,初始化语句需要更新

字面值常量类

除了算术类型、引用和指针外,某些类也是字面值类型。和其他类不同,字面值类型的类可能含有constexpr函数成员。这样的成员必须符合constexpr函数的所有要求,它们是隐式const的。

数据成员都是字面值类型的聚合类是字面值常量类;如果一个类不是聚合类,但符合下述要求,则它也是一个字面值常量类:

  1. 数据成员都必须是字面值类型
  2. 类必须至少含有一个constexpr构造函数
  3. 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
  4. 类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr构造函数

尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数,事实上一个字面值常量类必须至少提供一个constexpr构造函数。

constexpr构造函数可以声明成=default的形式或者是删除函数的形式。否则,constexpr构造函数就必须既符合构造函数的要求(不能包含返回语句),又符合constexpr函数的要求(唯一可执行语句是返回语句)。综上,constexpr构造函数体一般是空的,我们通过前置关键字就可以声明一个constexpr构造函数了。

class Debug{
public:
	constexpr Debug(bool b = true):hw(b), io(b), other(b) {}
	constexpr Debug(bool h, bool i, bool o):
		h(w), 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_other(bool b) {hw=b;}
private:
	bool hw;
	bool io;
	bool other;
};
//constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式

constexpr Debug io_sub(false, true, false);  //io_sub是什么???
if (io_sub.any())
    cerr<<"print appropriate error messages"<<endl;
constexpr Debug prod(false);
if (prod.any())
    cerr<<"print an error message"<<endl;

类的静态成员

有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。

声明静态成员

我们通过在成员的声明之前加上关键字static使得使得其与类关联在一起。静态成员可以是public的或private的,静态成员的类型可以是常量、引用、指针、类类型等等。

class Account{
public:
	void calculate() {amuount += amount * interestRate;} //成员函数不通过作用域运算符就能直接使用静态成员
	static double rate(){return interestRate;}
	static void rate(double);
private:
	std::string owner;
	double amount;
	static double interestRate;
	static double initRate)();	
};

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。因此每个Account对象包含俩个数据成员:owner、amount。只存在一个interestRate对象并且它被所有Account对象共享。

静态成员函数不与任何对象绑定在一起,它们不包含this指针。因此,静态成员函数不能声明为const的,而且也不能在static函数体内使用this指针

使用类的静态成员

使用作用域运算符直接访问静态成员。

double r;
r = Account::rate(); //使用作用域运算符访问静态成员

静态成员不属于类的某个对象,但我们可以使用类的对象、引用或指针来访问静态成员。

Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();    //通过Account的对象或引用
r = ac2->rate();   //通过指向Account对象的指针

定义静态成员

我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static,该关键字只出现在类内部的声明语句。

void Account::rate(double newRate)
{
	interestRate = newRate;
}
//定义并初始化一个数据成员
double Account::interestRate = initRate();

必须在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次。类似全局变量,静态数据成员定义在任何函数之外。一旦被定义就一直存在于程序的整个生命周期中。

静态成员的类内初始化

通常情况下,类的静态成员不应该在类的内部初始化。然而我们能为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,所以它们能用在所有适合于所有常量表达式的地方。

class Account{
public:
	static double rate(){return interestRate;}
	static void rate(double);
private:
	static constexpr int period = 30; //period是常量表达式
	double daily_tbl[period];
}

如果某个静态成员的应用场景仅限于编译器可以替换它的值的情况,则一个初始化的const或constexpr static不需要分别定义;相反,如果我们将它用于一个值不能替换的场景中,则该成员必须有一条定义语句

//一个不带初始值的静态成员的定义(如果在类的内部提供了初始值,那么成员的定义不能再指定一个初始值了)
constexpr int Account::period;  //初始值在类的定义内提供

Tips:即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。

静态成员能用于某些场景,而普通成员不能

静态数据成员可以是不完全类型。

静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属类的指针或引用

class Bar{
public:
	//...
private:
	static Bar mem1;  //正确:静态成员可以是不完全类型
	Bar *mem2; //指针成员可以是不完全类型
	Bar mem3; //错误:数据成员必须是完全类型
};

//静态成员和普通成员的另外一个区别是我们可以使用静态成员作为默认实参
class Screen{
public:
    //bkground表示一个在类中稍后定义的静态成员
    Screen& clear(char = bkground);
private:
    static const char bkground;
};
//非静态成员不能作为默认实参,因为它的值本身属于对象的一部分,无法提供一个对象以便从中获取成员值,引发错误
posted @ 2020-06-17 16:11  Rser_ljw  阅读(367)  评论(0编辑  收藏  举报