《C++ Primer》【Chapter 7】

chapter 7 类

类的两个基本能力:

  1. 数据抽象:定义数据成员和函数成员的能力。
  2. 封装:保护类的成员不被随意访问的能力。

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

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

7.1 定义抽象数据类型

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

Sales_data类

注意,定义在类内部的成员函数是 的inline内联函数。

struct Sales_data {
	//成员函数
	std::string isbin() 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&);

引入this

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。

//例如
total.isbn();	//相当于调用 Sales_data::isbin(&total)

在成员函数内部,可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员对直接访问都被看作this的隐式引用。并且,不允许改变this中保存的地址。

引入const成员函数

在成员函数的参数列表后紧跟const,将隐式修改this指针的类型为指向常量的常量指针。默认情况下,this是指向非常量类型的常量指针(Sales_data *const)。尽管this是隐式的,但它仍然需要遵循初始化规则(常量对象只能使用指向常量的指针存地址),意味着默认情况下,不能把this绑定到一个常量对象上。这一情况也就使得不能在一个常量对象上调用普通的成员函数(非常量)

this是隐式的且不会出现在参数列表中,所以需要在紧跟参数列表后面用const表示this是一个指向常量的常量指针。这样的成员函数也叫常量成员函数

常量成员函数不能改变调用它的对象的内容

类作用域和成员函数

类本身就是一个作用域!

编译器分两步处理类:

  1. 首先编译成员的声明
  2. 然后编译成员函数体

因此,成员变量和成员函数的定义顺序并不影响调用

类的外部定义成员函数

和平常定义函数没有区别,只需要将返回类型、参数列表、函数名与函数内部一致即可,另外需要使用作用域符号,指明是哪个类的成员函数。

double Sales_data::avg_price() const {
  
}

定义一个返回this对象的指针

Sales_data& Sales_data::combine(const Sales_data &rhs) {
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

定义类相关的非成员函数

类常常需要一些辅助函数,比如add,read和print等。尽管这些函数定义的操作从概念上来说属于类的接口的组成部分,但它们实际上并不属于类本身。

这些非成员函数的声明一般应与类声明在同一个头文件中。

定义read和print函数

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

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

两点注意:

  1. IO类型不能拷贝,所以作为参数应该用引用。
  2. print函数不负责换行,一般来说,执行输出任务的函数应该尽量减少对格式的控制,而是交给用户来决定。

定义add函数

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

构造函数

每个类都分别定义类它的对象被初始化的方式,类通过一个或多个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

默认构造函数

如果我们的类型没有显示的构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。也称合成的默认构造函数。规则如下:

  • 如果存在类内的初始值,用它来初始化成员。
  • 否则,默认初始化该成员。

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

  1. 编译器只有在发现类不包含任何构造函数的情况下,才会生成一个默认的构造函数。一旦有定义其他构造函数,除非再定义一个默认构造函数,否则类将没有默认构造函数。
  2. 对于某些类来说,合成的默认构造函数可能执行错误的操作。因为用户在创建类的对象时可能得到未定义的值。
  3. 编译器不能为某些类合成默认的构造函数。例如,如果其中一个类型成员没有默认构造函数,则无法合成。

定义Sales_data的构造函数

struct Sales_data {
	Sales_data = defalut;
  
	//成员函数
	std::string isbin() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
	double avg_price() const;

	//数据
	std::string bookNo;
	unsigned units_sold = 0;	//类内初始值,是否支持根据编译器而定
	double revenue = 0.0;
};

= 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) {}

构造函数的初始值是成员名字的一个列表,每个名字后面紧跟括号括起来(或者在花括号内)成员初始值。不同成员的初始化通过逗号分隔开来。

构造函数的函数体内都是空的,这是因为构造函数的唯一目的就是为数据成员赋初值。

拷贝、赋值和析构

除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。如果我们不主动定义这些操作,编译器将替我们合成它们。

某些类不能依赖于合成的版本

尽管编译器能够替我们合成拷贝、赋值和销毁的操作,但是对于某些类而言,合成的版本无法正常工作。特别是,当类需要分配类对象之外的资源时,合成的版本常常会失效。

7.2 访问控制与封装

为了加强类的封装性,C++使用访问说明符

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(隐藏)类的实现细节。
class Sales_data {
public:
	Sales_data() = defalut;
	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) {}
	//成员函数
	std::string isbin() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
private:
	double avg_price() const;

	//数据
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

使用class或struct关键字

struct和class都可以定义类,唯一的区别是,struct和class默认访问权限不一样。

类可以在它的一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式。但如果使用struct,则定义在第一个访问说明符之前的成员是public的;相反,如果使用class关键字,则这些成员是private的。

当我们希望定义的类的所有成员是public时,使用struct;反之,如果希望成员是private的,使用class。

友元

当Sales_data中的数据成员是private时,类相关的非成员函数read、print和add函数也就无法正常编译了,这是因为尽管这几个函数是类的接口的一部分,但它们不是类的成员。

类可以允许其他或者函数访问它的非公有成员,方法是令其他类或者函数为它的友元(friend)友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。一般,将友元在类定义开始前或者结束前的位置集中声明。

struct Sales_data {
  //非成员函数要访问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&);
public:
	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) {}
	//成员函数
	std::string isbn() const { return bookNo; }
	Sales_data& combine(const Sales_data&);
private:
	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&);

友元的声明

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

一个函数可以是多个类的友元函数,就像一个人可以是多个人的朋友一样。我觉得更通俗的理解应该是,类的非成员函数是一个独立的个体,类内声明这个非成员函数是我的朋友,可以访问类的非公有成员,但其他类也可以声明该函数是其他类的朋友。

封装的益处

  1. 确保用户代码不会无意间破坏封装对象的状态;

防止由于用户的原因造成数据被破坏。这样,也可以帮助我们快速定位因为类中实现逻辑导致的程序错误,因为私有数据只会在这些地方被使用。

  1. 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。

一旦把数据成员定义成private的,类的作者就可以比较自由的修改数据。当实现部分改变时,我们只需要检查类的代码本身以确认这次改变有什么影响;即只要类的接口不变(比如调用的接口名字),用户代码就无须改变。如果数据是public的,则所有使用了原来数据成员的代码(Sales_data.bookNo=id)都可能失效,我们必须定位并重写所有依赖于老版本实现的代码。

7.3 类的其他特性

类成员

可以在类中定义类型成员

class Screen{
public:
  using pos = std::string::size_type;
  typedef std::string:size_type pos;
}

与普通成员使用与定义顺序无关不同的是,类型成员必须在使用之前定义。

令成员作为内联函数

在类中,常有一些规模较小的函数适合于被声明成内联函数。

可以在类内部把inline作为声明的一部分显示地声明成员函数,同样的,也能在类的外部用inline关键字修饰函数的定义。需要注意的是,无须在声明和定义的地方同时说明inline,虽然这么做是合法的。最好是在类外部定义的地方说明inline(在外部的原因是内联函数虽然可以多次定义,但每次定义都必须完全一致,所以inline函数的定义应该和头文件在一起,即直接在头文件的类外部直接定义好)

重载成员函数

重载成员函数和重载普通函数一样

可变数据成员(mutable)

有时会出现希望修改类的某个数据成员,即使是在const成员函数。可以通过在变量声明中加入mutable实现。

一个可变数据成员永远不会是const,即使它是const对象的成员。

类数据成员的初始值

为了能让类开始时,总是拥有一个默认初始化的值,最好的方式就是把这个默认声明成一个类内初始值。

返回*this的成员函数

返回的是个对象,一般会定义成引用,不然会进行拷贝。

const成员的函数,返回的*this也是常量

基于const的重载

class Screen{
public:
  Screen &display(){
    return *this;
  }
  const Screen &display() const{
    return *this;
  }
}

当调用重载函数的对象是常量时,调用重载的const成员函数,反之,调用非常量成员函数。

类类型

每个类定义了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是两个不同的类型。

struct First{
  int memi;
  int getMem();
};
struct Second{
  int memi;
  int getMem();
}
First obj1;
Second obj2 = obj1;	//错误,两者类型不同

类也可以将声明和定义分开。

class Screen;

只声明的有时被称为前向声明。对于声明的类型,在声明之后定义之前是一个不完全类型。即我们只知道它是一个类,但不清楚包含哪些成员。

不完全类型使用场景非常有限,因为没有定义,编译器就不知道这样的对象需要多少存储空间。

注意:由于类必须定义后,编译器才知道要分配多少空间,所以一个类的成员不能是自己,但是一旦一个类的名字出现后,它就被认为声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针。

友元在探

除了前面友元需要注意的,友元不仅可以用于函数,还可以用于类型(即声明朋友关系)。

注意:友元关系不存在传递性(即你friend的friend,不是你的friend)

令成员函数作为友元

除了能让整个类作为友元之外,还可以精确到某个类的成员函数作为友元,只需要明确指出成员函数属于哪个类即可。

class Screen {
  friend void Window_mgr::clear(ScreenIndex);
}

为了实现这种友元,需要按步骤进行声明和定义

  1. 首先定义Window_mgr类,其中声明clear函数,但不能定义它。因为在clear使用Screen的成员之前必须先声明Screen。
  2. 接下来先定义Screen类,包括对于clear函数的友元声明。
  3. 最后定义clear,此时clear可以非常完美的使用Screen的成员。

函数重载和友元

当重载函数时,虽然名字相同,但都是不同的函数。所以若要使用友元,需要每个函数都加上friend。

友元声明和作用域

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

就算是在类的内部定义该友元函数,也必须在类的外部提供相应的声明从而使得函数可见。换句话说,仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的。

struct X {
  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的声明在作用域中出现了,使得函数可见。

7.4 类的作用域

一个类就是一个作用域,作用域外的想用类内的成员就必须要用作用域运算符。

名字查找与类的作用域

编写的一般程序中,名字查找的过程是:

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

对于定义在类内部的成员函数来说,解析其中名字的方式与上述查找规则有所区别,分两步处理:

  1. 首先,编译成员的声明。
  2. 知道类全部可见后才编译函数体。

编译器处理完类中的全部声明后才会处理成员函数的定义。这样,成员函数体中可以使用类中定义的任何名字。

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

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

类型名要特殊处理

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

typedef double Money;
class Account{
  typedef double Money;	//错误,不能重新定义
}

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

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

  1. 首先,在成员函数内查找该名字的声明。和前面一样,只有在函数使用之前出现的声明才被考虑。(成员函数内)
  2. 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑。(类内)
  3. 如果类内也没有找到该名字的声明,在成员函数定义之前的作用域内继续查找。(类外,成员函数定义之前)

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

外层作用域的名字,也可以显示地通过作用域运算符(::)进行请求。

int x = 4;
class A {
public:
	void fun(int x) {
		std::cout << ::x << std::endl;//结果为4
	}
};

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

名字查找第三步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域中的声明。只要函数定义中使用的某个名字在之前被声明了,都是ok的。

7.5 构造函数再探

构造函数初始值列表

初始化和赋值是有区别的。初始化时直接初始化数据成员,后者则先初始化再赋值如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在函数体之前执行默认初始化。例如:

//这种写法合法但比较草率,没有使用构造函数初始值,而是进行的先定义,赋值
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 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) { }	//正确,显示地初始化引用和const成员

与此同时,先定义再赋值的方式,效率上也不高,所以要养成使用构造函数初始值的习惯,这样能避免某些意向不到的编译错误,特别是遇到有的类含有需要构造函数初始值的成员时。

成员初始化的顺序

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。具体顺序与它们在类定义中的出现顺序一致。

为了忽略顺序的影响,最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可以的话,尽量避免使用某些成员初始化其他成员。

默认实参和构造函数

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

委托构造函数

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

struct Sales_data {
public:
    Sales_data(const std::string &s, unsigned n, double p):
    bookNo(s), units_sold(n), revenue(p*n) {}
    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& combine(const Sales_data&);
private:
    double avg_price() const;

    //数据
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。

默认初始化在以下情况下发生:

  • 当在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
  • 当类类型的成员没有在构造函数初始值列表中显示的初始化时。

值初始化在以下情况发生:

  • 在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时。
  • 当不使用初始值定义一个局部静态变量时。
  • 当通过T()的表达式显性地请求值初始化时。

使用默认构造函数

防止新手错误即可

Sales_data obj();	//这是定义函数,并不是定义对象
Sales_data obj2;	//正确

隐式的类类型转换

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

string null_book = "99999";
Sales_data item = Sales_data(null_book);

只允许一步类类型转换

//需要两步转换
//1."99999"转换成string
//2.string转换成Sales_data
item.combine("99999");	//错误

类类型转换不是总有效

是否需要转换,需要主观的判断,并不一定就是对的。

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

struct Sales_data {
public:
    Sales_data(const std::string &s, unsigned n, double p):
    bookNo(s), units_sold(n), revenue(p*n) {}
    Sales_data():Sales_data("", 0, 0){}
    explicit Sales_data(std::string s):Sales_data(s, 0, 0) {}
    explicit Sales_data(std::istream &is):Sales_data(){ read(is, *this); }
    Sales_data& combine(const Sales_data&);
private:
    double avg_price() const;

    //数据
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit的。且只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。

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

隐式转换的一种情况是当执行拷贝形式的初始化时(使用=)。此时,只能使用直接初始化,而不能使用explicit构造函数。

Sales_data item1(null_book);
Sales_data item2 = null_book;

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

尽管不能将explicit的构造函数用于隐式转换过程,但是可以使用构造函数显示地强制进行转换:

item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));	//正确的

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

一些例子:

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

聚合类

当类满足下列条件时是聚合类:

  • 所有成员都是public
  • 没有定义任何构造函数(空的都不可以,但是默认合成的可以即default)
  • 没有类内初始值
  • 没有基类,有没有virtual类

聚合类可以通过初始值列表初始化聚合类的成员:

struct Data {
    int ival;
    std::string s;
};

int main() {
	Data val = {1, "hello world"};
    std::cout<< val.s << std::endl;
}

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

显示地初始化类的对象的成员存在3个明显的缺点:

  • 要求类的所有成员都是public的
  • 将正确初始化每个对象的每个成员的任务交给了类的用户。
  • 添加或删除一个成员之后,所有的初始化语句都需要更新。

字面值常量类

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

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

constexpr构造函数

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

constexpr构造函数可以声明成=default的形式。否则,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): hw(h), io(i), other(b) {}
    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) {other=b;}
private:
    bool hw;
    bool io;
    bool other;
};

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

constexpr构造函数的作用是:生成constexpr对象以及constexpr函数的参数或返回类型

7.6 类的静态成员

有时候希望类的一些成员与类本身直接相关,而不是与类的各个对象保持关联。例如银行利率和用户之间的关系。

声明静态成员

我们通过在成员的声明之前加上关键字static。静态成员可以是public也可以是private的。

class Account {
public:
    void calculate() {amount += 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 = Account::rate();

虽然静态成员不属于某个类,但是仍然可以通过对象、引用或者指针来访问静态成员。

定义静态成员

既可以类内部定义,也可以外部定义。static关键字只出现在类内部的声明中!

void Account::rate(double newRate) {
    interestRate = newRate;
}
double Account::interestRate = initRate();	//不能在函数里调用

静态成员的类内初始化

通常,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。

class Account {
public:
    void calculate() {amount += amount * interestRate;}
    static double rate() { return interestRate;}
    static void rate(double);
private:
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
  	static constexpr int period = 30;	//period是常量表达式
};

即使一个常量静态数据成员在类内部被初始化类,通常情况下,也应该在类外部定义一下该成员,但不能再赋值了。

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

两个场景

  1. 静态数据成员可以是不完全类型(声明之后,定义之前的类型)
class Bar {
public:
  //......
private:
  static Bar mem1;	//正确,静态成员可以是不完全类型
  Bar *mem2;				//正确,指针成员可以是不完全类型
  Bar mem3;					//错误,数据成员必须是完全类型,此时Bar尚未定义完
}
  1. 静态成员可以作为默认实参

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

class Screen{
public:
  Screen& clear(char = background);
private:
  static const char background;
}
posted @   Dybala21  阅读(34)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
历史上的今天:
2019-08-08 2019HDU多校第一场 6582 Path 【最短路+最大流最小割】
2019-08-08 POJ_1273 Drainage Ditches 【网络流】
2019-08-08 2019HDU多校第六场 6641 TDL
点击右上角即可分享
微信分享提示