C++ Primer学习笔记-----第七章:类

本文详细探讨了类的基本思想,包括数据抽象、封装的实现方式,以及如何通过接口和实现分离。讲解了类的定义、成员函数、常量成员函数、友元和访问控制等内容,以及构造函数、拷贝与赋值、析构函数的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类的基本思想是数据抽象和封装。

1.数据抽象是一种依赖于接口和实现分离的编程技术。

类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,类的用户只能使用接口而无法访问实现部分。

类要想实现数据抽象和封装,需要首先定义一个抽象数据类型。在抽象数据类型中,由类的设计者负责思考类的实现过程;使用该类的程序员则只需要抽象的思考类型做了什么,而无须了解类型的工作细节。

我们可以把C++头文件当做接口,源文件当做实现

成员函数的声明必须在类的内部,它的定义可以在类的内部或外部。
作为接口组成部分的非成员函数,定义和声明都在类的外部
例如:

头文件:接口

struct Sales_data
{
	//函数成员:在类内部的函数时隐式内联的	
	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 add(const Sales_data&, const Sales_data&);

IO类属于不能被拷贝的类型,只能通过引用传递,而且要读取和写入,所以是普通引用
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);

源文件:实现

成员函数
#include "Sales_data.h"
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
   units_sold += rhs.units_sold;
   revenue += rhs.revenue;
   return *this;
}

double Sales_data::avg_price() const
{
    if(units_sold)
		return revenue/units_sold;
	else
	    return 0.0;
}

非成员函数
using std::istream;
using std::ostream;

Sales_data add(const Sales_data& lhs, const Sales_data& rhs)
{
	Sales_data sum = lhs;
	sum.combine(rhs);
	return sum;
}

istream& read(istream& is, Sales_data& item)
{
	double price = 0;
	is >> item.bookNo >> item.units_sold >> price;
	item.revenue = price * item.units_sold;
	return is;
}

ostream& print(ostream& os, const Sales_data& item)
{
	os << item.isbn() << " " << item.units_sold << " "
		<< item.revenue << " " << item.avg_price();
	return os;
}

引入this

例如:total.isbn();
使用点运算符来访问total对象的isbn成员,然后调用它。
当我们调用成员函数时,实际上是在替某个对象调用它。如果isbn指向Sales_data的成员(如bookNo),
则它隐式地指向调用该函数的对象的成员,如上调用中,当isbn返回boolNo时,实际上它隐式地返回total.bookNo。
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。
当我们调用一个成员函数时,用请求该函数的对象地址初始化this。例如调用total.isbn()
编译器负责把total的地址传递给isbn的隐式形参this,可以等价地认为编译器将该调用重写了如下形式:
Sales_data::isbn(&total)
调用Sales_data的isbn成员时传入total的地址。
this形参是隐式定义的,可以在成员函数体内部使用this,
std::string isbn() const { return this->bookNo; }
一般情况没有必要使用(形参和数据成员同名时用this来表明用类的数据成员)
this的目的总是指向"这个"对象,所以this是一个常量指针,不允许修改this中保存的地址。

***总结:this就是用什么调用就指向什么。

引入const成员函数

isbn()函数后面有const,作用是修改隐式this指针的类型。这样的成员函数被称为常量成员函数。
默认情况this的类型是指向类类型非常量版本的常量指针。例如在Sales_data成员函数中,this的类型是
Sales_data *const。尽管this是隐式的,但它仍然要遵循初始化规则,意味着(在默认情况下)我们不能把this
绑定到一个常量对象上,这一情况也就使得我们不能在一个常量对象上调用普通的成员函数。
把isbn函数体想象成如下的形式:注意如下的代码是非法的
std::string Sales_data::isbn(const Sales_data * const this) { return this->isbn; }
常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

***总结:常量成员函数就不能修改对象的内容

在类的外部定义成员函数

***总结:成员函数的定义必须与它的声明匹配,类外部定义的成员的名字必须包含它所属的类名
double Sales_data::avg_price() const
{
	if(units_sold)
		return revenue/units_sold;
	else
	    return 0.0;
}
编译器看到函数名,就能理解剩余的代码是位于类的作用域内的,因此能直接使用成员变量。

定义一个返回this对象的函数

函数combine的设计初衷类似于赋值运算符+=,调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象
则通过显示的实参被传入函数:
Sales_data& Sales_data::combine(const Sales_data& rhs)
{
   units_sold += rhs.units_sold;
   revenue += rhs.revenue;
   return *this;
}
+=赋值运算符返回的是左值,所以combine(模仿+=)也要返回左值,所以必须返回引用类型Sales_data&return语句解引用this指针以获得该函数的对象。

定义类相关的非成员函数

类的作者常常需要定义一些辅助函数,比如上面的add、read、print等,尽管这些函数定义的操作
从概念上来说属于类的接口的组成部分,实际上并不属于类本身。
如果函数在概念上属于类但不定义在类中,则它一般应与类声明(而非定义)在同一个头文件内,
在这种方式下,用户使用接口的任何部分都只需要引入一个文件。

构造函数

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
1.构造函数没有返回类型。
2.和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。
3.构造函数不能被声明成const,构造函数在const对象的构造过程中可以向其写值。
4.编译器创建的构造函数又被称为合成的默认构造函数。默认构造函数的按如下规则初始化类的数据成员:
	1>如果存在类内的初始值,用它来初始化成员。
	2>否则,默认初始化该成员。

某些类不能依赖于合成的默认构造函数

合成的默认构造函数只适合非常简单的类型。
对于普通的类,必须定义它自己的默认构造函数,原因有三:
1.编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的
  构造函数,除非我们再定义一个默认的构造函数,否则类奖没有默认的构造函数。
2.对于某些类来说,合成的默认构造函数可能执行错误的操作。
3.有时候编译器不能为某些类合成默认的构造函数。例如,如果类中包含一个其他类类型的成员且这个成员的类型
  没有默认构造函数,那么编译器将无法初始化该成员。
struct Sales_data
{
	//构造函数
	Sales_data() = 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;
};

C++11中,如果我们需要默认的行为,可以在参数列表后面写上= default来要求编译器生成构造函数。
=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。
如果=default在类的内部,则默认构造函数时内联的,否则不是内联的。

拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。
拷贝:如,初始化变量以及以值的方式传递或返回一个对象等
赋值:使用赋值运算符会发生对象的赋值操作
析构:当对象不再存在时执行销毁的操作
如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员
执行拷贝、赋值和销毁操作。

***某些类不能依赖于合成的版本***
特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。
不过值得注意的是,很多需要动态内存的类能(而且应该)使用vector对象或者string对象管理必要的存储空间。
使用vector或者string的类能避免分配和释放内存带来的复杂性。

2.访问控制和封装

定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
定义在private说明符之后的成员可以被类的成员函数访问,private部分封装了类的实现细节。
使用classstruct定义类唯一的区别就是默认的访问权限:class默认private,struct默认public

友元

struct Sales_data
{
	//友元的声明:仅仅指定了访问的权限
	friend Sales_data add(const Sales_data&, const Sales_data&);
	friend std::ostream& print(std::ostream&, const Sales_data&);
	friend std::istream& read(std::istream&, Sales_data&);
	
	//构造函数
	Sales_data() = 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 add(const Sales_data&, const Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);

add、print、read无法访问Sales_data的private的数据成员,可以使用友元解决
类可以允许其他类或者函数访问它非公有成员,方法是令其他类或者函数成为它的友元。
友元的声明只能出现在类定义的内部,具体位置不限,友元不是类的成员也不受它所在区域的访问控制级别约束。
一般最后在类的定义开始或结束前的位置集中声明友元。

友元的声明仅仅指定了访问权限,而非一个通常意义上的函数声明,如果我们希望类的用户能够调用某个友元函数,
那么我们就必须在友元声明之外在专门对函数进行一次声明。

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。

****注:许多编译器并为强制限定友元函数必须在使用之前在类的外部声明。
****及时编译器支持这种行为,最好还是提供一个独立的函数声明

3.类的其他特性

头文件

为了展示新特性,需要定义一对相关联的类,Screen和Window_mgr。
Screen表示显示器中的一个窗口

class Screen
{	
public:	
	//可以在类中自定义某种类型在类中的别名,两种方式,必须先定义后使用
	//typedef string::size_type pos;
	using pos = string::size_type;
	
	Screen() = default;
	Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht* wd, c) {}
	
	char get()const { return contents[cursor]; }	//隐式内联
	inline char get(pos ht, pos wd)const;			//显式内联
	Screen& move(pos r, pos c);
private:
	pos cursor = 0;
	pos height = 0, width = 0;
	string contents;
};

源文件

#include "Screen.h"

令成员作为内联函数,inline成员函数应该与相应的类定义在同一个头文件中
inline
char Screen::get(pos r, pos c) const
{
	pos row = r * width;
	return contents[row + c];
}

Screen& Screen::move(pos r, pos c)
{
	pos row = r * width;
	cursor = row + c;
	return *this;
}

可变数据成员

class Screen
{
public:
	void some_Fun() const;
private:
	//可变成员变量
	mutable size_t access_ctr = 0;
}

const成员函数可以改变一个可变成员的值
void Screen::some_Fun() const
{
	++access_ctr;
}

类数据成员的初始值

#include <vector>
#include "Screen.h"

class Window_mgr
{
public:
	using ScreenIndex = std::vector<Screen>::size_type;
	void clear(ScreenIndex);
private:
	当我们提供一个类内初始值时,必须以符合=或者或括号表示
	std::vector<Screen> screens{ Screen(24,80,' ') };
};

从const成员函数返回*this

一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用,常量引用不能修改数据
Screen& display(std::ostream& os) const
{
	do_display(os);
	return *this;
}

下面提供两个重载
class Screen
{	
public:			
	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;	}
};

上面单独定义一个do_display的原因:
1.避免在多处使用同样的代码
2.预期随着类的规模发展,display函数有可能变得更加复杂,写在一处比两处的作用就比较明显了
3.很可能在开发过程中给do_display函数添加调试信息,在最终版本中会去掉,在一处添加删除更容易
4.这个额外的函数调用不会增加任何开销,因为在类的内部定义,是隐式内联的。

类的声明

class Screen;		//类的声明

这种声明是前向声明,它向程序中引入了名字Screen并且指明Screen是一种类类型。
不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但不能定义)
以不完全类型作为参数或返回类型的函数。

对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。否则,编译器就无法了解
这样的对象需要多少存储空间。类似的,类也必须首先被定义,然后才能引用或者指针访问其成员。毕竟,如果
类尚未定义,编译器也就不清楚该类到底有哪些成员。

一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针:
class Link_screen
{
	Screen window;
	Link_screen* next;
	Link_screen* prev;
}

有一种例外:直到类被定义之后数据成员才能被声明成这种类型,constexpr,后面说

友元再探

之前Sales_data类把三个普通的非成员函数定义成了友元,类还可以把其他的类定义成友元,也可以把其他类的成员
函数定义成友元,友元函数能定义类的内部,这样函数是隐式内联。

类之间的友元关系

如果类都在各自的头文件里,直接声明友元类没有问题。
如果两个类在同一个文件里,还是有顺序要求的
1.函数实现在类内,一定是N在前定义,后面才是M的定义
class M;	//这个声明无所谓,如果需要可以声明

class N
{
	friend class M;
private:
	int num = 3;
};

class M
{
public:
	void Fun() {
		N n;
		n.num++;
	};
};

2.函数实现在类外,M和N没有顺序要求,但Fun的定义一定是在M和N定义的后面,C++是顺序编译
class M;  //可以不声明,没影响

class N
{
	friend class M;
private:
	int num = 3;
};

class M
{
public:
	void Fun();
};

void M::Fun()
{
	N n;
	n.num++;
}

令成员函数作为友元

***提示:要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。***
//1.先定义M和声明Fun
class M
{
public:
	void Fun();
};
//2.再定义N和Fun的友元声明
class N
{
	//这里要用M::Fun(),所以上面必须现有M::Fun()的声明
	friend void M::Fun();
private:
	int num = 3;
};
//3.最后定义Fun
void M::Fun()
{
	//这里要用N,所以上面一定要有N的声明
	N n;
	//这里要用N的成员,所以上面一定要有N中成员的声明
	n.num++;
}

例如上面的例子:如果想让M的一个Fun函数作为N的友元
1.首先定义M类,其中声明Fun函数,但不能定义它。在Fun使用N的成员之前必须先声明N.
2.接下来定义N,包括对于Fun的友元声明。
3.最后定义Fun,此时它才可以使用N的私有成员。

友元总结:要用什么,一定要提前声明什么,这样肯定不会错

友元声明和作用域

类和非成员函数的声明不是必须在它们的友元声明之前。
当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域中时可见的。
然而,友元本身不一定真的声明在当前作用域中。
甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。
换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,他也必须是被声明过的:
struct X
{
	//一定要记住:friend只是指定某个函数的权限,必须在外面声明该函数
	friend void f(){/*友元函数可以定义在类的内部*/}
	X(){ f(); }		//错误:f还没有被声明
	void g();
	void h();
}
void X::g(){ return f(); }	//错误:f还没有被声明
void f();					//声明那个定义在X中的函数
void X::h(){ return f();} 	//正确:现在f的声明在作用域中了

***注:有的编译器并不强制执行上述关于友元的限制规则。

4.类的作用域

在类的外部定义成员函数时必须同时提供类名和函数名,在类的外部,成员的名字被隐藏起来了,一旦遇到类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括
参数列表和函数体,结果就是,我们可以直接使用类的其他成员而无须在此授权。
另一方面,函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是那个类的成员。
例如,我们可能向Window_mgr类添加一个新的名为addScreen的函数,返回类型是ScreenIndex:
class Window_mgr
{
public:
	ScreenIndex addScreen(const Screen&);
}
//定义,ScreenIndex是Window_mgr里的类型
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen& s){/*****/}

4.1.名字查找与类的作用域

在目前为止,我们编写的程序中,名字查找(寻找与所用名字最匹配的声明的过程)的过程比较直截了当:
1.首先,在名字所在的块中寻找其声明语句,只考虑在名字的使用之前出现的声明。
2.如果没找到,继续查找外层作用域。
3.如果最终没有找到匹配的声明,则程序报错。

对于定义在类内部的成员函数来说,解析其中的名字的方式与上述的查找规则有所区别,类的定义分两部处理:
1.首先,编译成员的声明。
2.直到类全部可见后才编译函数体。
按照这种两阶段的方式处理类可以简化类代码的组织方式。

*****用于类成员声明的名字查找*****
这种两阶段的处理方式只适用于成员函数中使用的名字。声明中使用的名字,包括返回类型或者参数列表中使用的名字,
都必须在使用前确保可见。如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找
例如:
typedef double Money;
string bal;
class Account
{
public:
	Money balance() { return bal; }
private:
	Money bal;
}
当编译器看到balance函数的声明语句时,它将在Account类的范围内寻找对Money的声明。编译器只考虑Account中在使用
Money前出现的声明,因为没有找到匹配的成员,所以编译器会接着到Account的外层作用域中查找。在这个例子中,编译器
会找到Money的typedef语句,该类型被用作balance函数的返回类型以及数据成员bal的类型。
另一方面,balance函数体在整个类可见后才被处理,因此,该函数的return语句返回名为bal的成员,而非外层作用域
string对象。

*****类型名要特殊处理*****
一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。
然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字:
typedef double Money;
class Account
{
public:
	Money balance(){return bal;}	//使用外层作用域的Money
private:
	typedef double Money;			//错误:不能重新定义Money,即使类型和外层作用域一致
	Money bal;
}
注:尽管重新定义类型名字是一种错误的行为,但是编译器并不为此负责,一些编译器扔将顺利通过这样的代码,而忽略
代码有错的事实。------->说的就是你VS。
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

*****成员定义中的普通块作用域的名字查找*****
成员函数中使用的名字按照如下方式解析:
1.首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。
2.如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。
3.如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。

*****类作用域之后,在外围的作用域中查找*****
如果编译器在函数和类的作用域中都没有找到名字,它将在外围的作用域中查找。
使用外层作用域的成员用  ::成员名

*****在文件中名字的出现处对其进行解析*****
当成员定义在类的外部时,名字查找的第三部不仅要考虑定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前
的全局作用域中的声明。

******************************************
总结:使用什么要先声明什么

5.构造函数再探

*****构造函数的初始值有时必不可少*****
有时我们可以忽略数据成员初始化和赋值之间的差异,但并非总能这样。
如果成员是const或者是引用的话,必须将其初始化。
类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。例如:
class ConstRef
{
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int& ri;
}
ConstRef::ConstRef(int ii)
{
	//赋值
	i=ii;
	ci=ii;		//错误:不能给const赋值
	ri=i;		//错误:ri没被初始化
}
随着构造函数体一开始执行,初始化就完成了。
初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值,正确形式:
ConstRef::ConstRef(int ii)i(ii),ci(ii),ri(i){}
***建议:使用构造函数初始值

*****成员初始化的顺序*****
成员的初始化顺序与它们在类定义中出现顺序一致。与构造函数初始值列表中初始值的前后顺序无关。
但如果一个成员是用另一个成员来初始化的,那这两个成员的初始化顺序就很关键了。

*****默认实参和构造函数*****
class A
{
public:
	A(string s = "") : str(s){}
private:
	string str;
}

*****委托构造函数*****
class A
{
public:
	A(string s,string s2) : str(s) , str2(s2){}
	委托构造函数
	A() : A("",""){}
	A(string s) : A(s,""){}
	A(string s2) : A("",s2){}
private:
	string str;
	string str2;
}

*****默认构造函数的作用*****
当对象被默认初始化或值初始化时自动执行默认构造函数。
默认初始化:
1.当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
2.当一个类本身含有类类型的成员且使用合成的默认构造函数时。
3.当类类型的成员没有在构造函数初始值列表中显示地初始化时。
值初始化:
1.在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
2.当我们不使用初始值定义一个局部静态变量时。
3.当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名

不那么明显的一种情况是类的某些数据成员缺少默认构造函数:
class NoDefault
{
public:
	NoDefault(const string&);
}

struct A
{
	NoDefault mem;
}
A a;					//错误:不能为A合成构造函数,因为NoDefault没有默认构造函数
struct B
{
	B(){}				//错误:mem没有初始值,会自动执行默认构造函数,但是没有
	NoDefault mem;		
}

*****使用默认构造函数*****
class A{}
A a;	//使用默认构造函数
A a();	//声明了一个返回值为A的函数名为a的函数

隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。把这种构造函数称作转换构造函数。
class A
{
public:
	A(){}
	A(string s):str(s){}
	void Fun(A a){}
private:
	string str;
}

string s1 = "123";
A a;
a.Fun(s1);		//编译器自动给s1创建一个A对象,新生成的对象被传递给了Fun

*****只允许一步类类型转换*****
在上面的例子里,编译器只会自动执行一步类型转换。例如:
a.Fun("123");	//错误
原因:
1.先把"123"转换成string,因为"123"是字符串字面值,不是string类型的
2.在把这个临时string转换成A

想完成上述调用,可以显示地把字符串转换成string或者A
a.Fun(string("123"));	或者	a.Fun(A("123"));

*****类类型转换不是总有效*****
比如:转换后的对象没有意义

*****抑制构造函数定义的隐式转换*****
在构造函数前添加:explicit,关键字explicit只对一个实参的构造函数有效。
需要多个实参的构造函数不能用于执行隐式转换,所以无限将这些构造指定为explicit。
只能在类内声明构造函数时使用explicit关键字
class A
{
public:
	A(){}
	explicit A(string s):str(s){}
	void Fun(A a){}
private:
	string str;
}

A a;
a.Fun(string("123"));   //错误,没有隐式转换
a.Fun(A("123")			//正确:显示转换

*****为转换显示地使用构造函数*****
a.Fun(A("123"));
a.Fun(static_cast<A>("123"));	//字符串字面值自动转换为string,这里是把string转换为A

*****标准库中含有显示构造函数的类*****
我们用过的一些标准库中含有单参数的构造函数:
1.接受一个单参数的const char*的string构造函数,不是explicit2.接受一个容量参数的vector构造函数,是explicit

聚合类

聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下条件:
1.所有成员都是public的。
2.没有定义任何构造函数。
3.没有类内初始值。
4.没有基类,也没有virtual函数。

例如:
struct Data
{
	int i;
	string s;
}

我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:顺序要和声明的一致
Data vall = {0,"a"};
显示初始化类的对象的成员存在三个明显的缺点:
1.要求所以成员都是public2.将正确初始化每个对象的每个成员的重任交给了类的用户,因为用户很容易忘掉某个初始值或不恰当的值,过程冗长容易出错。
3.添加和删除一个成员之后,所有初始化语句都需要更新。

字面值常量类

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

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

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

constexpr构造函数可以声明成=default的形式,否则constexpr构造函数就必须既符合构造函数的要求(不能包含返回语句),
有符合constexpr函数的要求(唯一可执行语句就是返回语句)。综上,constexpr构造函数体一般来说应该是空的。

class A
{
public:
	constexpr A(bool b = true) : bb(b),bb2(b) {}
private:
	bool bb;
	bool bb2;
}

constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或者是一条常量表达式。

6.类的静态成员

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

*****声明静态成员*****
class A
{
public:
	static int a;			//这是声明
	static void Fun();
private:
	static int get();
}
静态成员存在于任何对象之外,它是属于类的。
静态成员函数也不与任何对象绑定,它们不包含this指针。所以,静态成员函数不能声明成const的,
也不能在static函数体内使用this指针。

*****使用类的静态成员*****
1.使用作用域运算符直接访问静态成员:
	A::a++;
	A::Fun();
2.虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员:
	A a1;
	a.a++;
	a.Fun();

*****定义静态成员*****
既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,
该关键字只出现在类内部的声明语句:
void A::Fun() {}

静态数据成员不属于类的任何一个对象,所以它们不是在创建类的对象是被定义的。意味着它们不是由类的构造函数初始化的。
类似于全局变量,静态数据成员定义在任何函数之外。一旦被定义,一直存在于程序的整个生命周期中。
定义静态数据成员的方式和在类的外部定义成员函数差不多。需要指定对象的类型名,然后是类名、作用域运算符、成员名字。
int A::a = get();		//定义并初始化一个静态成员
从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了,所以可以访问私有函数get。

建议:要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。

*****静态成员的类内初始化*****
通常情况下,类的静态成员不应该再类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,
不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,
所以它们能用在所有适合于常量表达式的地方。
class A
{
	static constexpr int num = 3;			//声明:num是常量表达式
	int arr[num];							//常量表达式可以用来初始化其他成员
}

constexpr int A::num;		//定义:如果类的内部提供了一个初始值,则成员的定义不能在指定一个初始值了
如果num唯一用途就是定义arr的维度,则不需要在A外面专门定义num。
如果num还有其他用途,例如:把num传递给一个接受const int&的函数时,则必须定义num。

*****静态成员能用于某些场景,而普通成员不能*****
举个例子:
静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则收到限制,
只能声明成它所属类的指针或引用:
class B
{
private:
	static B b1;		//正确:静态数据成员可以是不完全类型
	B *b2;				//正确:指针成员可以是不完全类型
	b b3;				//错误:数据成员必须是完全类型
}
另外一个区别:可以使用静态成员作为默认实参
class B
{
public:
	void Fun(int = cc);	//cc表示一个在类中稍后定义的静态成员
private:
	static const int cc;
}
const int B::cc = 666;

void B::Fun(int c)
{
	std::cout << c;
}

非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象
以便从中获取成员的值,最终将引发错误。

类的这一章东西比较多,需要多看看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值