C++Primer笔记-类

前言

该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。

本文大致整理了第七章的知识点,涉及到C++关于类的重要知识,内容比较多,但是需要重点掌握。

链接目录

定义抽象数据类型

const成员函数:默认情况下,this指向类类型的非常量版本的常量指针(内容可变,指向不可变),该指针无法绑定到常量对象上,所以常量对象无法调用普通的成员函数(因为普通成员函数并没有限制写操作)。通过修改this成为指向常量的指针,可以绑定常量对象,但此时只能调用常量函数。在参数列表之后添加const关键字,可以让该成员函数成为常量成员函数。常量成员函数可以读取对象的数据成员,但不能写入值。

定义类:classstruct的区别主要在于class的成员默认是private的,而struct默认是public

class/struct MyClass{
public:
	...
private:
	...
protected:
	...
};

在类的外部定义成员函数:外部声明的函数和内部的声明需要保持一致,同时也需要添加所属的类名。

//const是否添加取决于成员函数类型
double MyClass::func() const{
	/* ... */
}

定义一个返回this对象的函数:注意返回类型是引用

MyClass& MyClass::func2(){
	return *this;//返回调用该函数的对象
}

构造函数

构造函数用来初始化对象的数据成员,对于const对象也仍然可以写值(对象完成构造函数之后,才真正能够拥有常量属性)。

默认构造函数:如果没有显式地定义构造函数,则会隐式地定义一个

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

注意:

  • 只有类没有声明任何构造函数时,才会自动生成默认构造函数。
  • 对于复合类型,默认构造函数的初始化可能导致未定义的值,而使用他们将导致错误,所以对于类的声明,构造函数尽量显式定义。

default:告诉编译器该函数完全等同于默认构造函数。=default可以在内部声明时出现,也可以在类外部定义时出现,如果在类的内部,则默认构造函数是内联的,如果在外部,则默认情况不是内联的。

class MyClass{
public:
	MyClass() = default;
	MyClass(int a);
	...
private:
	int m_a;
};

构造函数初始值列表:

MyClass(int a) : m_a(a){}

在类的外部定义构造函数:

MyClass::MyClass(int a){
	m_a = a;
}

拷贝、赋值和析构

执行赋值语句:

total = trans;
//编译器自动合成的操作,对数据成员进行赋值操作,等价于
total.m_a = trans.m_a;

某些类不能依赖于合成的版本:管理动态内存的类通常不能依赖于上述操作的合成版本,需要自定义赋值操作(vector或string能够正常工作)。

更详细的拷贝控制会在第十三章进行介绍,这里简单带过。

访问控制与封装

访问说明符:

  • public:在整个程序内可以被访问
  • private:只可以被类的成员函数访问

友元

对于某些函数是类接口的一部分,但不是类的成员,此时无法访问非公有成员,所以提出友元,能够让非类成员访问私有数据。

class MyClass{
	friend int getData(MyClass&);
public:
	...
private:
	int m_a;
};
int getData(MyClass& mc){
	return mc.m_a;
}

类可以把其他的类定义成友元,也可以把其他类的成员函数定义成友元。

类之间的友元关系:友元不具有传递性,如果Window_mgr有自己的友元,Window_mgr的友元可以访问Window_mgr,但这个友元不能访问Window_mgr的友元(Screen)。

class Screen{
	friend class Window_mgr;
};

令成员函数作为友元:

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

该方法必须仔细组织结构以满足声明和定义的依赖关系:

  • 首先定义Window_mgr类,其中声明clear函数,但不能定义。在clear使用Screen的成员之前必须先声明Screen
  • 接下来定义Screen,包括对clear友元的声明。
  • 最后定义clear,这样才能使用Screen的成员。

函数重载和友元:重载函数名字相同,但本质仍然是不同的函数,所以想要把一组函数声明成它的友元,则需要对这组函数的每一个分别声明。

extern ostream& storeOn(ostream&, Screen &);
extern BitMap& storeOn(BitMap &, Screen &);
class Screen{
	//如果只声明了重载的其中一个,则另一个不能访问Screen的私有成员
	friend ostream& storeOn(ostream &, Screen &);
};

类成员

定义一个类型成员:

//这里的size_type是一种unsigned的整数类型
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;
}

令成员作为内联函数:在类内部的成员函数是自动inline的,我们也可以在类的外部显式地声明为inline的成员函数。

inline void Screen::func(...){
	...
}

注意:无需在声明和定义的地方同时说明inline,虽然是合法的,但是最好只在类外部定义的地方说明inline。

可变数据成员:修改类的某个数据成员,即使是在一个const成员函数内。通过加入mutable关键字,可用于记录函数调用次数等信息。

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

返回*this的成员函数:

class Screen{
public:
	Screen &set(char);
	Screen &move(char);
};
inline Screen &Screen::set(char c){
	return *this;
}
inline Screen &Screen:::move(char c){
	return *this;
}

//函数返回引用才能修改本对象,否则修改的是临时对象
myScreen.move(..).set(..);
//等价于
myScreen.move(..);
myScreen.set(..);

//若返回的不是引用,则等价于
Screen temp = myScreen.move(..);
temp.set(..);

从const成员函数返回*thisconst成员函数返回的*this是常量引用。

基于const的重载:区分成员函数是否是const,可以进行重载。

当一个成员函数调用另一个成员函数时,this指针隐式传递。所以非常量版本的func调用func2时,this指针会隐式地转换成指向常量的指针。

class MyClass{
public:
	void func(...){
		//因为func2是const函数,执行该函数时this指针隐式转化
		func2();
		...
	}
	//const对象调用的版本
	void func(...) const{
		func2();
		...
	}
private:
	void func2(...) const;
}

类类型

类的声明:不完全类型(已声明但未定义)可以定义指向这种类型的指针或引用,通常用于类包含类自己。

//前向声明,在声明之后定义之前是不完全类型
class Screen;

类的作用域

遇到类名之后,剩余部分在作用域之内,例如:

//Screen是类作用域内的类型,但不用再次指定
void Window_mgr::clear(Screen i){
	...
}

返回类型通常在类名之前,不在作用域之内,在返回类型是类内定义的类型时需要指定,例如:

//Screen是类内定义的类型,需要指定
Window_mgr::Screen Window_mgr::addScreen(const Screen &s){
	...
}

名字查找与类的作用域

通常的名字查找过程:

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

对于定义在类内部的成员函数:

  • 首先编译成员的声明。
  • 直到类全部可见后才编译函数体。

成员函数中的名字解析:

  • 首先在成员函数内查找名字声明(只考虑使用之前出现的声明)。
  • 如果函数内没找到, 则在类内查找,所有成员都考虑。
  • 如果类内没找到,在成员函数定义之前的作用域内继续查找。

类作用域之外的名字查找:

void Screen::fcn(pos height){
	cursor = width * ::height;
	//类内存在height的成员,同时也有名为height的全局变量
	//此时的height为全局变量
}

构造函数再探

该方法和构造函数初始值列表有些许不同,该方法在赋值之前对数据成员执行默认初始化。

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

当成员是const或是引用时,必须对其进行初始化,上述方法将产生错误,必须通过构造函数初始值列表进行初始化。

建议:使用构造函数初始值进行初始化,一是关乎底层效率,二是一些数据成员必须被初始化。

成员初始化的顺序:
在构造函数初始值列表中,顺序取决于类定义中成员的出现顺序。如果一个成员是由另一个成员初始化时,就需要注意。

class X{
	int i;
	int j;
public:
	//i未定义,实际上i先定义,再定义j,此时j是未定义的,所以i未定义
	X(int val):j(val), i(j){}
};

默认实参和构造函数:

class MyClass{
public:
	//提供了默认实参空字符串,当没参数时,使用默认实参
	MyClass(int a = 0):m_a(a){}
}

委托构造函数

class Sales_data{
public:
	MyClass(int a):m_a(a){}
	//委托构造函数
	MyClass() : MyClass(0){}//委托给带参的构造函数
};

聚合类

当一个类满足如下条件时,它是聚合的:

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。

例如:

struct Data{
	int ival;
	string s;
};

//我们可以使用花括号对聚合类进行列表初始化
Data dt = {10, "hello"};

字面值常量类

数据成员都是字面值类型的聚合类(字面值常量类),如果一个类不是聚合类,但满足如下条件,也算是字面值常量类:

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

类的静态成员

声明静态成员:加上关键字static,可以是public或private。
静态成员函数不包含this指针,也不能声明成const(const修饰的函数必须含有this指针)。

class Account{
public:
	void calculate();
	static double rate();
private:
	double amount;
	static double interestRate;
}

使用类的静态成员:

double r = Account::rate();//通过作用域运算符
Account ac1;
r = ac1.rate();//通过Account的对象或引用
//对于成员函数来说,可以不使用作用域运算符

定义静态成员:在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句。

静态成员的类内初始化:通常静态成员不应该在类内部初始化。

静态成员能用于某些场景,而普通成员不行:静态数据成员可以是不完全类型,而非静态成员只能声明它所属类的指针或引用。

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

静态成员可以作为默认实参,普通成员不行。

未列入的知识点

explicit:抑制隐式类型转换(P263)

上一篇 下一篇

评论 | 0条评论