C/C++教程

《C++Primer》第七章——类

本文主要是介绍《C++Primer》第七章——类,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

第七章:类

类的基本思想是数据抽象封装
数据抽象是一种依赖于接口实现分离的编程技术
类的实现包括类的数据成员负责接口实现的函数体以及定义类所需的各种私有函数

7.1 定义抽象数据类型

1.类的所有成员都必须在类的内部声明,但是成员函数体可以定义在类的内部也可以定义在类外
2.this
1)this形参是隐式定义的,因此任何自定义名为 this 的参数或变量的行为都是违法的
2)可以在成员函数体内部使用 this
3)this 总是指向“这个”对象,保存对象的地址,任何对类成员的直接访问都被看作 this 的隐式引用,因此 this 是一个常量指针,不允许改变 this 中保存的地址
3.引入 const 成员函数:const 的作用是修改隐式 this 指针的类型。
1)默认情况下,this 的类型是指向类类型非常量版本的常量指针,尽管 this 是隐式的,但仍需要遵循初始化规则,因此我们不能把 this 绑定在一个常量对象上
2)若某个成员函数的函数体内不会改变 this 所指的对象,则把 this 设置为指向常量的指针有助于提高函数的灵活性
3)C++中允许将 const 关键字放在成员函数的参数列表之后,此时紧跟在参数列表后面的 const 表示 this 是一个指向常量的指针,像这样使用 const 的成员函数被称作常量成员函数
4)若 this 是指向常量的指针,则常量成员函数不能改变调用它的对象的内容
5)常量对象,以及常量对象的引用或指针都只能调用常量成员函数
4.类作用域和成员函数
1)类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内
2)对于类的编译,编译器分两步处理:首先编译成员的声明,然后才轮到成员函数体,因此,成员函数体可以随意使用类中的成员而无需在意这些成员出现的次序(成员可以定义在函数体之后)
5.在类的外部定义成员函数:当在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配,即返回类型、参数列表和函数名都得与类内部的声明保持一致;若成员被声明为常量成员函数,则它的定义也必须在参数列表后明确指定 const 属性;类外部定义的成员的名字必须包含它所述的类名

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

6.构造函数

  • 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数称为构造函数,构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数;
  • 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数称为构造函数,构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数;
  • 构造函数的名字和类名相同,和其他函数不同的是,构造函数没有返回类型
  • 类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量参数类型上有所区别;
  • 构造函数不能声明为 const 的,当我们创建类的一个 const 对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”属性,因此,构造函数在 const 对象的构造过程中向其写值
    1)合成的默认构造函数:类通过一个特殊的构造函数来控制默认初始化过程,这个函数称为默认构造函数,默认构造函数无需任何实参;如果我们的类没有显示地定义构造函数,那么编译器会为我们隐式地定义一个默认构造函数,编译器创建的构造函数又被称为合成的默认构造函数,对于大多数类,合成的默认构造函数将按如下规则初始化类的数据成员
  • 如果存在类内初始值,则用它来初始化成员
  • 否则,默认初始化该成员
    2)某些类不能依赖于合成的默认构造函数:合成的默认构造参数只适合非常简单的类,对于一个普通的类,必须定义自己的默认构造函数,原因有以下三点
  • 只有当类没有任何声明构造函数时,编译器才会自动地生成默认构造函数,一旦我们定义了其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数;
  • 若定义在块中的内置类型或复合类型(比如指针和数组)的对象被默认初始化,则它们的值是未定义的,因此,含有内置类型或复合类型成员的类应该在类的内部初始化这些成员或者定义一个自己的默认构造函数;
  • 有时编译器不能为某些类合成默认的构造函数,比如类中包含一个其他类类型的成员且这个成员的类型没有默认构造函数,则编译器无法初始化该成员
    3)=default:在 C++11 新标准中,若我们需要默认的行为,则可以通过在参数列表后面写上 =default 来要求编译器生成构造函数
Sales_data() = default;

因为该构造函数不接受任何实参,因此是一个默认构造函数,定义这个构造函数的目的仅仅是因为我们既需要其他形式的构造函数,而需要默认的构造函数。
4)构造函数初始值列表:构造函数初始值列表是成员名字的一个列表,每个名字后面紧跟括号括起来的成员初始值,不同成员的初始化通过逗号分隔开来,当某个数据成员被构造函数初始值列表忽略时,它将以合成默认构造函数相同的方式隐式初始化

//其他两个成员以合成默认构造函数相同的方式隐式初始化,本例中,由于另两个成员有类内初始值,因此使用类内初始值初始化
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) { }

7.拷贝、赋值和析构
1)除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为

  • 当初始化变量以及以值得方式传递或返回一个对象时,会发生拷贝
  • 当使用赋值运算符时会发生对象的赋值操作
  • 当对象不再存在时执行销毁的操作,比如一个局部对象会在创建它的块结束时被销毁
    2)如果不主动定义这些操作,编译器将会替我们合成,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁工作,但对于某些类来说合成的版本无法正常工作,特别是当类需要分配类对象之外的资源时,合成的版本往往会失效

7.2 访问控制与封装

1.在C++语言中,使用访问说明符加强类的封装性:

  • 定义在 public 说明符后的成员在整个程序内可被访问,public 成员定义类的接口
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了类的实现细节
    2.一个类可以包含0个或多个访问说明符,每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止
    3.class 和 struct:使用 class 和 struct 定义类唯一的区别就是默认的访问权限,如果使用 struct 关键字,则定义在第一个访问说明符之前的成员是 public 的;相反,若使用 class 关键字,则这些成员是 private 的
    4.友元:类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以 friend 关键字开头的函数声明语句即可
    一般来说,最好在类定义开始或者结束前的位置集中声明友元
    5.友元的声明:友元的声明仅指定了访问权限,而非一个通常意义上的函数声明,若希望类的用户能够调用某个友元函数,则必须在友元声明之外再对函数进行一次声明。
    为了使永远对类的用户可见,通常将友元的声明和类本身放置在同一个头文件中

7.3 类的其他特性

1.如果我们提供了构造函数,因此编译器不会再自动生成默认的构造函数,如果我们的类仍需要默认构造函数,则必须显示地声明出来,比如:通过 =default 告诉编译器为我们合成默认的构造函数
2.令成员作为内联函数:常有一些规模较小的函数适合于被声明为内联函数
1)定义在类内部的成员函数时自动 inline 的,称为隐式内联
2)可以在类的内部把 inline 作为声明的一部分显示地声明成员函数
3)也可以在类的外部用 inline 关键字修饰函数的定义
注:虽然无须在声明和定义的地方同时说明 inline,但这么做也是合法的,但最好只在类外部定义的地方说明 inline,这样更易理解
3.可变数据成员:有时我们希望即使在一个 const 成员函数内,也能修改类的某个数据成员,可以通过加 mutable 关键字达到这一点

class Screen{
public:
	void some_member() const;
private:
	mutable size_t access_ctr;		//因为mutable,即使在一个const对象中也能被修改
};
void Screen::some_member() const
{
	++access_ctr;		//保存一个计数值,用于记录成员函数被调用的次数
}

4.类数据成员的初值:类内初始值必须使用 = 的初始化形式或者花括号括起来的直接初始化形式

class Screen{
public:
	typedef std::string::size_type pos;
	//...
private:
	pos cursor = 0;				//使用 = 的初始化形式
	pos height = 0, width = 0;
	std::string contents;
};

class Window_mgr{
private:
	std::vector<Screen> screens{Screen(24, 80, '')};	//使用花括号括起来的直接初始化形式
};

5.返回 this 的成员函数
1)返回引用的函数是左值的,意味着这些函数返回的是
对象本身而非对象的副本
2)一个 const 成员函数如果以引用的形式返回 *this 那么它的返回类型将是
常量引用*,而该返回值则不可以的调用普通成员函数,因为普通成员函数会修改属性,因此返回的常对象只能调用常函数
3)通过区分成员函数是否是 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;}

Screen myScreen(5, 3);
const Screen blank(5, 3);
myScreen.set('#').display(cout);	//调用非常量版本
blank.display(cout);				//调用常量版本

6.类类型:
1)每个类定义了唯一的类型,对于两个类来说,即使成员完全一样,这两个类也是两个不同的类型
2)类的声明:就像函数的声明和定义分离开一样,我们也可以只声明类而暂时不定义它

class Screen;	//Screen类的声明

这种声明也称为前向声明,但在它声明之后定义之前是一种不完全类型,此时知道 Screen 是一个类类型,但是不清楚到底包含哪些成员
不完全类型只能在非常有限的情景下使用:

  • 可以定义这种类型的指针或引用
  • 可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数
    7.友元再探
  • 类可以把普通的非成员函数定义为友元
  • 类可以把其他的类定义为友元
  • 类也可以把其他类(之前已经定义过的)的成员函数定义成友元
    注:友元函数可以定义在类的内部,这样的函数是隐式内联的
    1)类之间的友元关系
  • 如果一个类指定了友元类,则友元类的成员函数可以访问此类包括所有非公有成员在内的所有成员
  • 友元关系不存在传递性
    2)令成员函数作为友元:要想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系
  • 首先定义 Window_mgr 类,其中声明 clear 函数,但是不能定义它
  • 接下来定义 Screen,包括对于 clear 的友元声明
  • 最后定义 clear,此时它才可以使用 Screen 的成员
    3)函数重载和友元:如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明
    4)友元声明和作用域:即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的
struct X{
	friend void f()	{/* 友元函数可以定义在类的内部*/ }
	X() {f();}				//错误:f还没有被声明	
	void g();
	void h();
};
void X::g() {return f();}
void f();
void X::h() {return f();}

上述代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。

7.4 类的作用域

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

Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s)
{
	screens.push_back(s);
	return screens.size() - 1;
}

2.名字查找与类的作用域
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 对象
3)类型名的定义通常应该出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名定义之后
4)成员函数中使用名字的具体解析方式

  • 首先,在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才被考虑
  • 如果在成员函数内没有找到,则在类内部继续查找,这时类的所有成员都可以被考虑
  • 如果类内也没有找到该名字的声明,在成员函数定义之前的作用域中继续查找
//注意:通常情况下,不建议为参数和成员使用同样的名字
int height;		//定义了一个名字,稍后在Screen中使用
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;
};

对于上述程序,当编译器处理 dummy_fcn 中的乘法表达式时,它首先在函数作用域内查找表达式中用到的名字,函数的参数位于函数作用域内,因此用到的名字 height 是该函数的参数说明
因此,上述程序 height 参数隐藏了同名的成员,如果想绕开上面的查找规则,可以改为下述代码

void Screen::dummy_fcn(pos height){
	cursor = width * this->height;		//成员height,而非参数height
	//另外一种表示该成员的方式
	cursor = width * Screen::height;	//成员height,而非参数height

当然,最好的确保我们使用 height 成员的方法是给参数起个别的名字

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

在上述代码中,编译器无法在 dummy_fcn 函数中找到名字 height,因此编译器会接着在 Screen 类内查找匹配的声明,即使 height 的声明出现在 dummy_fcn 使用它之后,编译器也能正确的解析函数使用的是名为 height 的成员
类作用域之后,在外围的作用域中查找:如果编译器在函数和类的作用域中均没有找到名字,则会接着在外围的作用域中查找

void Screen::dummy_fcn(pos height){
	cursor = width * ::height;		//显示的使用了全局作用域运算符,因此是全局的height
}

7.5 构造函数再探

1.构造函数的初始值有时必不可少
1)如果成员是 const 或 引用的话,必须将其初始化,类似的,当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化
2)随着构造函数体一开始执行,初始化就完成了,我们初始化 const 或者引用类型的数据成员的唯一机会就是通过构造函数初始值,因此如果成员是 const、引用或者属于某种默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值

class ConstRef{
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;
};

//错误:ci 和 ii必须被初始化
//这个版本是对数据成员执行了赋值操作
ConstRef::ConstRef(int ii)
{
	i = ii;		//正确,赋值操作
	ci = ii;	//错误:不能给const赋值
	ri = i;		//错误:ri是引用,引用必须被初始化,而该程序中ri没被初始化
}

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

3)使用构造函数初始值:在很多类中,初始化赋值的区别事关底层的效率问题,前者直接初始化数据成员,而后者则先初始化再赋值,除了效率问题,一些数据成员必须被初始化,因此建议养成构造函数初始值的习惯,可以避免某些意想不到的编译错误
4)成员初始化顺序:成员的初始化顺序与它们在类的定义中的出现顺序一致,而构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序,因此,最好令构造函数初始值的顺序与成员声明的顺序保持一致,而且如果可能的话,尽量避免使用某些成员初始化其他成员

class X{
	int i;
	int j;
public:
	//错误,i在j之前初始化,而此时j是未定义的
	X(int val):j(val), i(j){ }
};

2.委托构造函数:一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。

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);}

注:当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行,只有受委托的初始值列表和函数体被依次执行后,该构造函数的函数体才会被执行
3.默认构造函数的作用:当对象被默认初始化或值初始化时,自动执行默认构造函数
1)默认初始化在以下情况下发生:

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
  • 当类类型的成员没有在构造函数初始值列表中显式地初始化时
    2)值初始化在以下情况下发生:
  • 在数组初始化过程中如果我们提供的初始值数量小于数组大小时
  • 当我们不使用初始值定义一个局部静态变量时(P 185)
  • 当我们通过书写形如 T( ) 的表达式显式地请求值初始化时,其中 T 是类型名 (P 88)
    3)使用默认构造函数:如果想定义一个使用默认构造函数进行初始化的对象,正确用法是去掉对象名之后的空括号对
Sales_data obj();	//错误:声明了一个函数而非对象
Sales_data obj2;	//正确:obj2是一个对象而非函数

4.隐式的类类型转换
1)能通过一个实参调用的构造函数定义了一条从构造函数的实参类型向类类型隐式转换的规则,需要多个实参的构造函数不能用于执行隐式转换

string null_book = "9-999-99999-9";
//构造一个临时的Sales_data对象
//该对象的units_sold和revenue等于0,bookNo等于null_book
item.combine(null_book);

在上述程序中,编译器用给定的 string 自动创建了一个 Sales_data 对象,新生成的这个临时 Sales_data 对象被传递给 combine,因为 combine 的参数是一个常量引用,因此我们可以给该参数传递一个临时量
2)只允许一步类类型转换:编译器只会自动地执行一步类型转换

//错误:需要用户定义的两种转换
//(1)把“9-999-99999-9”转换成string
//(2)再把这个临时的string转换成Sales_data
item.combine("9-999-99999-9");

//正确:显示地转换成string,隐式地转换成Sales_data
item.combine(string("9-999-99999-9");
//正确:隐式地转换成string,显示地转换成Sales_data
item.combine(Sales_data("9-999-99999-9");

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

  • 通过将构造函数声明为 explicit 阻止隐式转换,关键字 explicit 只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将这些构造函数指定为 explicit 的
class Sales_data{
public:
	Sales_data() = default;
	//多个实参的构造函数不能执行隐式转换,因此也不用声明 explicit
	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) { }
	explicit Sales_data(std::istream&);
  • 只能在类内声明构造函数时使用 explicit 关键字,在类外部定义时不应重复(与 friend 关键字不同,声明定义时重复使用也可)
//错误:explicit关键字只允许出现在类的构造函数声明处,而非定义处
explicit Sales_data::Sales_data(istream& is)
{
	read(is, *this);
}
  • explicit构造函数只能用于直接初始化:发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(使用=),因此声明了 explicit 的构造函数只能直接初始化
Sales_data item1(null_book);	//正确:直接初始化
//错误:不能将explicit构造函数用于拷贝形式的初始化过程
Sales_data item2 = null_book;

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

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

5.聚合类(p 266)
6.字面值常量类(p 267)

7.6 类的静态成员

有时类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持互连
1)声明静态成员

  • 通过在成员的声明之前加上关键字 static 使得其与类类型关联在一起
  • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
  • 静态成员函数也不和任何对象绑定在一起,它们不包含 this 指针,静态成员函数不能声明为 const 的,也不能在 static 函数体内使用 this 指针
    2)使用类的静态成员
  • 使用作用域运算符直接访问静态成员
double r;
r = Account::rate();	
  • 虽然静态成员不属于类的某个对象,但仍可使用类的对象、引用或者指针来访问静态成员
Account ac1;
Account *ac2 = &ac1;
//调用静态成员函数rate的等价形式
r = ac1.rate();		//通过Account的对象或引用
r = ac2->rate();	//通过指向Account对象的指针
  • 成员函数不用通过作用域运算符就能直接使用静态成员
    3)定义静态成员
  • 和其他成员函数一样,既可以在类的内部也可在类的外部定义静态成员函数,静态成员函数并不属于某一对象,所有对象共有,没有this指针,不能访问本类中非静态成员,可以说静态成员函数的出现就是为了处理静态成员变量的
  • 类的外部定义静态成员函数时,不能重复 static 关键字,该关键字只出现在类内部的声明语句
  • 静态数据成员不属于类的任何一个对象,因此它们并不是在创建类的对象时被定义,也意味着它们不是由类的构造函数初始化的,一般来说,不能再类的内部初始化静态成员,而必须在类的外部定义和初始化每个静态成员,和其他对象一样,一个静态数据成员只能定义一次
  • 为了确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中
    4)静态成员的类内初始化
  • 通常情况下,类的静态成员不应该在类的内部初始化,这是因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,该静态对象会被定义多次,这是严重错误的;也可以这么理解,static 数据成员在类外定义和初始化是为了保证只被定义和初始化一次,这样编译器就不必考虑类的函数里面第一个对static变量的’=’操作是赋值还是初始化了
  • 但是,有一种例外,我们可以为静态成员提供 const 整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的 constexpr,是因为它既然是 const 的,那程序就不会再去试图初始化了
    5)静态成员能用于某些场景,而普通成员不能
  • 静态成员独立于任何对象,静态数据成员可以使不完全类型(类在声明后定义前为一个不完全类型),特别的,静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制,只能给你声明为它所属类的指针和引用
  • 静态成员可以作为默认实参,而非静态数据成员不能作为默认实参
    可以这么理解:编译器对默认实参的解释和翻译是在编译链接期,而非运行时。因此,作为默认实参的对象要么是一个常量,要么有一个在编译期就能够确定的地址,比如静态变量。而this指针及其成员的地址在编译期还不确定,所以不行。
这篇关于《C++Primer》第七章——类的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!