类 & 对象

定义

概念 描述
类成员函数 类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
类访问修饰符 类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。
构造函数 & 析构函数 类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。
C++ 拷贝构造函数 拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
C++ 友元函数 友元函数可以访问类的 private 和 protected 成员。
C++ 内联函数 通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。
C++ 中的 this 指针 每个对象都有一个特殊的指针 this,它指向对象本身。
C++ 中指向类的指针 指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。
C++ 类的静态成员 类的数据成员和函数成员都可以被声明为静态的。

与 struct 区别

  1. 成员的默认访问权限不同 :class 默认 private,struct 默认 public
  2. class 可以用在模板中代替 typename,struct 不能。

可以看到区别很小 ,c++保留 struct 更多是为了兼容 C。但是语义上一般倾向于把struct当作C时代的struct来用,即只有成员变量,没有逻辑(或只有极其简单的数据存入读取逻辑),用来把多个变量打包成一个类型,而不用struct来做面向对象编程意义上的class。

抽象类

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。

类成员函数

成员函数可以定义在类定义内部,或者单独使用范围解析运算符::来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。

1
2
3
4
5
6
7
8
9
class Box
{
public:
// ...
double getVolume(void)
{
return length * breadth * height;
}
};

也可以在类的外部使用范围解析运算符::定义该函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Box
{
public:
double length; // 长度
double breadth; // 宽度
double height; // 高度

// 成员函数声明
double getVolume(void);
};

// 成员函数定义
double Box::getVolume(void)
{
return length * breadth * height;
}

类访问修饰符

数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。

每个标记区域在下一个标记区域开始之前都是有效的。成员和类的默认访问修饰符是 private。

公有成员

公有成员在类的外部可访问。

私有成员

私有成员变量或函数在类的外部不可访问,只有类和友元函数可以访问私有成员。

保护成员

保护成员变量或函数可被派生类中的任何成员函数访问。除此外和私有成员相同。

类构造函数 & 析构函数

类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(double len); // 这是构造函数

private:
double length;
};

Line::Line( double len)
{
cout << "Object is being created, length = " << len << endl;
length = len;
}
...
int main( )
{
Line line(10.0);
...
return 0;
}

使用初始化列表来初始化字段

假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化 ,可以:

1
2
3
4
C::C( double a, double b, double c): X(a), Y(b), Z(c)	// 最好要按照变量在类声明的顺序一致
{
....
}

复制控制

除了成员函数、构造函数定义了类型的对象的操作,类还可以通过特殊的成员函数(复制构造函数、赋值操作符、析构函数)来控制复制赋值撤销该类型对象时会发生什么。

类的析构函数

类的析构函数会在每次删除所创建的对象时执行。当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象超出作用域时,才会执行析构函数。

一般而言,如果需要析构函数,那么也需要赋值操作符和复制构造函数。

析构函数的名称与类的名称相同,在前面加~作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 这是构造函数声明
~Line(); // 这是析构函数声明

private:
double length;
};
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << endl;
}
...

拷贝构造函数

拷贝构造函数通常用于:

  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。

编译器会默认定义拷贝构造函数。如果类带有指针变量,或者有动态内存分配,则它必须有一个拷贝构造函数。

因为默认的拷贝构造函数实现的是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。

所以我们就必须定义一个深拷贝构造函数,为新的对象分配单独的内存资源。

拷贝构造函数的最常见形式如下:

1
2
3
classname (const classname &obj) {	// obj 是一个对象引用,该对象是用于初始化另一个对象的。
// 构造函数的主体
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
using namespace std;
class CExample
{
private:
int a;

public:
//构造函数
CExample(int b)
{
a = b;
cout << "creat: " << a << endl;
}

//拷贝构造
CExample(const CExample &C)
{
a = C.a;
cout << "copy" << endl;
}

//析构函数
~CExample()
{
cout << "delete: " << a << endl;
}

void Show()
{
cout << a << endl;
}
};

//全局函数,传入的是对象
void g_Fun(CExample C)
{
cout << "test" << endl;
}

int main()
{
CExample test(1);
//传入对象
g_Fun(test);

return 0;
}

如何防止默认拷贝发生

声明一个私有的拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类的对象,编译器会报告错误,从而可以避免按值传递或返回对象。

当出现类的等号赋值时,会调用拷贝函数

友元函数

友元机制允许一个类将对其非公有成员的访问权授予指定函数或类。友元不是授予友元关系那个类的成员,所以它们不受其声明出现部分的访问控制影响。

友元可以是一个函数,该函数被称为友元函数;也可以是一个类,称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Box
{
double width;

public:
friend void printWidth(Box box);
friend class BigBox;
void setWidth(double wid);
};

class BigBox
{
public:
void Print(int width, Box &box)
{
// BigBox是Box的友元类,它可以直接访问Box类的任何成员
box.setWidth(width);
cout << "Width of box : " << box.width << endl;
}
};
// 注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}

// 程序的主函数
int main()
{
Box box;
BigBox big;
// 使用成员函数设置宽度
box.setWidth(10.0);
// 使用友元函数输出宽度
printWidth(box);
// 使用友元类中的方法设置宽度
big.Print(20, box);
return 0;
}

作用域

友元声明将已命名的类或非成员函数引入外围作用域中。此外友元函数可以在类内部定义,该函数的作用域扩展到包围该类定义的作用域。

内联函数

由于一般调用函数需要许多工作:保存寄存器,复制实参,跳转到入口地址,返回时恢复寄存器等等。

内联函数可以避免函数调用时的开销。在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。函数名前面放置关键字 inline就可定义为内联函数。

对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

一般而言,内联机制适用于优化比较小的、经常被调用的函数。绝大多数编译器不支持递归函数的内联。

在类定义中的定义的函数都是内联函数。

this 指针

每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

1
2
3
4
5
6
7
8
9
10
class Box
{
public:
double Volume() {
return length * breadth * height;
}
int compare(Box box) {
return this->Volume() > box.Volume();
}
};

指向类的指针

一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 **->**,就像访问指向结构的指针一样。与所有的指针一样,必须在使用指针之前对指针进行初始化。

1
2
3
4
5
6
7
8
9
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box *ptrBox; // Declare pointer to a class.
ptrBox = &Box1;

cout << "Volume of Box1: " << ptrBox->Volume() << endl;
return 0;
}

类的静态成员

静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化

静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。如果不加定义就会报错,初始化是赋一个初始值,而定义是分配内存。

  • 常量变量:必须通过构造函数参数列表进行初始化。
  • 引用变量:必须通过构造函数参数列表进行初始化。
  • 普通静态变量:要在类外通过”::”初始化。
  • 静态整型常量:可以直接在定义的时候初始化。
  • 静态非整型常量:不能直接在定义的时候初始化。要在类外通过”::”初始化。
1
2
3
4
5
6
7
8
9
class Box
{
public:
static int objectCount;
Box(double l=2.0, double b=2.0, double h=2.0) {
objectCount++;
}
};
int Box::objectCount = 0;

静态成员函数

如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。

静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。

静态成员函数有一个类范围,他们不能访问类的 this 指针。可以使用静态成员函数来判断类的某些对象是否已被创建。

静态成员函数与普通成员函数的区别:

  • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
  • 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。