C++Primer第五版【笔记】——第七章 类

是数据的抽象和封装。数据抽象是一种将接口和实现分离的设计技术。接口是指用户可以对类使用的操作集。实现包括类的数据成员和接口函数体。封装使得类的使用者不必关注类内部是如何实现的,因为这些是类的设计者需要关注的。

1 抽象数据类型

定义在类中的函数默认为内联的(inline)。类的成员函数必须在类内部声明,函数定义可以放在类的内部或外部。

1.1 this指针

每一个类的内部都有一个隐含的this指针,该参数是由系统负责维护。它的类型是CLASSTYPE *const this;即指向某个类的const指针。所以this指针在初始化以后就不能改变。系统使用this指针来指明函数使用的是哪个实例的数据成员。

在调用成员函数时,系统会自动传递类实例的地址给this指针:

CLASSTYPE exm;
exm.func();
可以将该函数调用理解为: CLASSTYPE::func(&exm);

1.2 const成员函数

正如1.1中所说,在调用成员函数时,会传递类实例的地址给this指针。如果该实例是const对象,那么非const指针是无法指向const对象的。可以在函数参数列表后加上const来表明是const成员函数。
int CLASSTYPE::func() const;
【注】const对象或指向const对象的指针或引用只能调用const成员函数。

1.3 构造函数

构造函数的作用是初始化类对象的数据成员。没有返回值。
class CTest{
public:
	CTest() = default;
	CTest(int a1):a(a1){}
	int GetA() const {return a;}
private:
	int a;
};
【注】声明为const的类对象 const CTest cc;,在调用构造函数初始化对象以后,才具有const的属性。所以构造函数不能声明为const。
如果没有显式定义构造函数,编译器会自动生成一个不带参数的 合成默认构造函数。该默认构造函数所做的事是使用 默认机制初始化数据成员。即:
  • 如果有类内初始化,则使用类内的初始化值初始化。
    class CTest{
    ...
    private:
    	int a = 0; // 类内初始化,c++11标准
    };
  • 否则,对于内置类型初始化为默认值(与局部变量初始化方式一样,一般为未知的值,所以最好进行初始化),对于类类型则调用该类的构造函数进行初始化。
当定义有构造函数时,编译器不会生成合成默认构造函数,如果想将不含参数的构造函数设置为和合成默认构造函数一致,可以在函数后添加= default。
既然编译器可以自动合成,为什么要自己定义构造函数?原因有三:
  1. 只有当没有自定义构造函数时,编译器才会自动合成;
  2. 合成默认构造函数的行为有时候并不是我们想要的;
  3. 有时候编译器不能自动生成构造函数。比如当某个类类型数据成员具有自定义构造函数时。

构造函数初始化列表

在执行函数体之前,编译器先根据构造函数初始化列表对每个数据成员初始化。如果省略了某个数据成员,且该成员在类内有初始化值,则根据类内的初始化值初始化,否则根据合成默认构造函数的规则对其初始化。

2 访问控制和封装

2.1 访问控制

  • public:标识为public的成员可以被外部程序访问。一般用来定义接口。
  • private:私有成员只能被类内部成员或者标识有friend的类或函数访问。private封装的类的实现。

classstruct的区别

class默认的访问权限是private,struct默认的访问权限是public。

2.2 友元

如果想让某个类或外部函数访问类的私有成员,可以在类内添加 friend声明,一般放在类的开头或结尾。
class CFriend{
};

void go();

class CTest{
	friend CFriend;
	friend void go();
public:
	CTest() = default;
	CTest(int a1):a(a1){}
	int GetA() const {return a;}
private:
	int a;
};
【关于封装】
封装的好处:
  1. 可以防止用户无意中改变类中封装的数据成员的状态。
  2. 将成员函数的实现封装起来,可以方便后续的修改和维护。因为只要类的接口不变,类内部封装部分的修改,不会导致用户代码的更改。当然,修改后的源文件需要重新编译。

3 类的其他特性

3.1 有关类成员

3.1.1 在类中定义类型别名

在类内定义类型别名的方法:
class CType{
public:
	typedef vector<int>::size_type viSize;
};
使用新标准的方法:
class CType{
public:
	using viSize = vector<int>::size_type;
};
类内部变量的声明可以不需要担心位置问题,而类型的定义一般需要放在类的开头,因为在使用类型前必须声明。

3.1.2 关于inline

在类内部定义的成员函数是默认inline的。当然也可以显式的声明一个成员函数为Inline。可以在类内声明,或者在类外定义时声明。一般将inline声明放在类外定义时。
inline函数的定义和类的定义应该放在同一个头文件中。

3.1.3 mutable数据成员

声明为mutalbe的数据成员不会被const所限定,即使是声明为const的对象。所以,const成员函数可以改变mutable数据成员。

3.2 返回*this的函数

返回*this的函数的返回值类型是类的引用。该类型返回值是左值。这种函数的好处是可以连写。比如: cout << "hello" << " world!" << endl;
#include <iostream>
   
   
    
    
using namespace std;

class RThis {
public:
	RThis &Get() { return *this; }
	int  Go(int b) { return a = b; }
	int A() { return a; }
private:
	int a;
};

int main()
{
	RThis obj1;
	obj1.Get().Go(10);
	cout << obj1.A() << endl;	
	return 0;
}
   
   
对于上面的类的一个对象obj1,调用 obj1.Get().Go(10); 则a的值会变为10。而如果Get()函数的返回值为RThis而不是引用,则a的值不会改变。因为*this返回后是赋值给一个临时对象,Go(10)改变的是临时对象中的a。

3.3 类类型

我们可以像声明内置类型一样声明一个类。
class ClassDecl;
此时的类是一个非完整类型(incomplete type)。我们可以定义一个指向该类型的指针或引用,可以声明一个包含非完整类型的参数或返回值的函数。但是, 不能用非完整类型创建该类的对象。因为,编译器还不知道该类的内部成员,无法为对象分配内存空间。
class ClassDecl{
	ClassDecl *pDec;  // ok
	ClassDecl &rDec;  // ok
	ClassDecl Dec; // error
};

3.4 再说友元

前面提到过说可以在类中声明一个类或函数为friend。我们还可以声明一个类中的某个成员函数为其他类的友元。声明与定义的顺序要注意:
class CFriend{
public:
	void go();
};

class CTest{
	friend void CFriend::go();	
};

void CFriend::go()
{
}
  1. 先定义CFriend类,go()函数需要声明但是不能定义;因为在go()函数可以使用CTest类的成员之前,CTest类必须定义。
  2. 定义CTest类,包含friend声明。
  3. 定义go()函数。
【注】friend声明只影响可访问性,不能代替一般意义上的声明。

4 类范围

在类外实现的成员函数需要在函数名前面指定类名和作用域符号,此后函数的参数列表,函数体都存在于该类的范围内。但是返回值是例外,需要单独指定。
class CTest{
	typedef unsigned UINT;
public:
	UINT go(UINT);
};

CTest::UINT CTest::go(UINT a) {
	return a;
}

4.1 名字查找

当我们使用一个名字时,编译器需要查找与该名字对应的声明。查找方法:
  1. 在该名字所在的程序块内查找。只考虑在该名字之前出现的声明。
  2. 如果没有找到,则在程序块外查找。
  3. 如果没有找到,则程序出错。
而类的成员函数中使用的名字的查找规则有些不同:
  1. 首先,在函数体内,该名字出现之前查找它的声明。
  2. 如果在函数体内没有找到,则在类中查找。
  3. 如果在类中也没有找到,则在该函数定义出现之前查找。
可见成员函数的定义是在所以声明之后才编译,所以成员函数可以使用类内部任意位置出现的名字。
上面的规则只适用于成员函数体内的名字查找,对于函数参数列表和函数返回值中的名字查找规则,和一般的规则一样。
class CTest{	
public:
	UINT go(UINT); // error,UINT在使用前必须先声明
	typedef unsigned UINT;
};

5 再看构造函数

当我们定义一个变量时,一般会对它进行初始化,而不是在定义完之后对其赋值。
int a = 1; // 定义并初始化
int b;   // 定义,b被默认初始化,如果该定义出现在函数内部,则b的值未定义;如果出现在函数外部,则b=0
b = 1;
我们知道如果一个数据成员没有出现在构造函数的初始化列表中,那么编译器会对其进行默认初始化

5.1 有时构造函数初始化器是必须的

初始化和赋值的区别在一般情况下可以不用计较。但是const和引用成员必须初始化,此时要和赋值区别开。同样的,没有默认构造函数的类类型的成员也必须初始化。 类的初始化结束于构造函数体开始执行前,即必须在构造函数初始化列表中初始化。
编译器不是按照参数初始化列表中变量定义的顺序进行初始化,而是根据变量在类中定义的顺序初始化。没有出现在初始化列表中的变量会默认初始化。

5.2 委托构造函数(Delegating Constructors)

【C++11】
新标准扩展了构造函数初始化的使用。在委托构造函数的初始化列表中,只有一个调用另一个构造函数的入口。
class CTest{	
public:
	CTest(int ia):a(ia){}
	CTest():CTest(0){}
private:
	int a;
};
将初始化的任务由其他构造函数代理,这就是委托构造函数

5.3 默认构造函数的职责

当一个对象被 默认初始化值初始化时,会自动使用默认构造函数(内置类型为默认初始化器)。
发生默认初始化的情况:
  • 在程序块范围内定义的非静态变量或数组没有初始化时
  • 当一个类中包含有使用合成默认构造函数的类类型的成员时
  • 当类类型成员没有在构造函数初始化列表中显式初始化时
发生值初始化的情况:
  • 当初始化一个数组,提供的初始化少于数组长度时
  • 当定义了一个没有初始化的局部静态对象时
  • 当明确需要值初始化时(比如vector使用一个参数的构造器,用一个参数指定元素的个数)
class CFriend{
public:
	CFriend(int ib):b(ib){}
private:
	int b;
};

class CTest{	
public:
	CTest(int ia):a(ia),cf(0){}
	
private:
	int a;
	CFriend cf;
};
上面的例子中,如果在CTest的构造函数中,不显式的调用CFriend的构造函数初始化cf,则会发生错误。因为CFriend没有默认构造函数。所以 总是提供一个默认构造函数是一个好习惯

5.4 隐式类类型转换

每一个带有一个参数的构造函数都定义了一个隐式的类类型转换。这种构造函数也称为 转换构造函数(converting constructor)。
class Addor{
public:
	Addor(int ad):ad1(ad){}	
	void add(const Addor &a) { ad1 += a.ad1;}
private:
	int ad1;
};

Addor addor(0);
addor.add(10);
当addor.add(10)调用时,常量10会先转换成addor类型,因为Addor类中定义了一个带有int参数的构造函数。
【注】在一个表达式中只允许一次隐式类类型转换。
有时我们并不想让这种隐式的类类型转换发生。使用 explicit声明可以终止这种隐式转换。
使用explicit声明的构造函数,只能使用直接初始化,不能使用赋值形式的初始化。
class Addor{
public:
	explicit Addor(int ad):ad1(ad){}	
	Addor(string str):s(str){}
	
private:
	int ad1;
	string s;
};
...
Addor addor1("ok"); // error,两次类类型转换
Addor addor2 = 10;  // error,explicit构造函数 不能使用赋值形式的初始化
虽然explicit声明的构造函数不能进行隐式类类型转换,但是可以使用 static_cast显式转换。
库函数中string的,有一个const char*类型的构造函数不是explicit,vector有一个size参数的构造函数是explicit。

5.5聚合类

满足以下条件:
  • 所以数据成员是公有的
  • 没有构造函数
  • 没有类内初始化
  • 没有基类或虚函数
聚合类就好比是C语言中的struct结构体中加入了一些函数。
struct Data{
  int data;
  int total;
  int add(int a) {
    total += a;
  }
};

6 static类成员

static类成员,包括数据成员和成员函数,属于类,而不属于一个特定的类对象。
static成员函数没有this指针,不能声明为const,在函数体内部也不能使用this。
static成员的引用可以通过类名+作用域操作符的方式,或者通过对象。
class Test{
public:
	static void go();
};
void Test::go()
{
}
static只需要在类内声明,在类外定义是不需要声明。
static数据成员的初始化一般和非内联函数的定义放在一起。
class Test{
public:
	static void go();
private:
	static int size ;
};
int Test::size = 10;
void Test::go()
{
}


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superbin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值