您的位置:首页 > 编程语言 > C语言/C++

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

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

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。
既然编译器可以自动合成,为什么要自己定义构造函数?原因有三:

只有当没有自定义构造函数时,编译器才会自动合成;
合成默认构造函数的行为有时候并不是我们想要的;
有时候编译器不能自动生成构造函数。比如当某个类类型数据成员具有自定义构造函数时。

构造函数初始化列表

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

2 访问控制和封装

2.1 访问控制

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

class和struct的区别

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;
};
【关于封装】
封装的好处:

可以防止用户无意中改变类中封装的数据成员的状态。
将成员函数的实现封装起来,可以方便后续的修改和维护。因为只要类的接口不变,类内部封装部分的修改,不会导致用户代码的更改。当然,修改后的源文件需要重新编译。

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

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

4 类范围

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

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

4.1 名字查找

当我们使用一个名字时,编译器需要查找与该名字对应的声明。查找方法:

在该名字所在的程序块内查找。只考虑在该名字之前出现的声明。
如果没有找到,则在程序块外查找。
如果没有找到,则程序出错。
而类的成员函数中使用的名字的查找规则有些不同:

首先,在函数体内,该名字出现之前查找它的声明。
如果在函数体内没有找到,则在类中查找。
如果在类中也没有找到,则在该函数定义出现之前查找。

可见成员函数的定义是在所以声明之后才编译,所以成员函数可以使用类内部任意位置出现的名字。

上面的规则只适用于成员函数体内的名字查找,对于函数参数列表和函数返回值中的名字查找规则,和一般的规则一样。
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()
{
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息