目录:点我
一、类
一般来说。类规范由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口;
- 类方法定义:描述如何实现类成员函数。
基本定义格式如下,通常数据为私有型,方法为公有型:
class classNanme {
private:
data member declarations
public:
member function prototypes
};
如此便实现了对于数据的隐藏,用户只能通过公有方法来对数据实现处理。
私有类型是类的默认访问控制,因此可以不声明 private 。
在类外可以用作用域控制符实现对类方法的定义:
class className {
public:
void show();
};
void className::show() {
...
}
访问控制:关键字 private, public, protected
描述了对类成员的访问控制,同时还尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏是一种封装;将实现的细节隐藏在私有部分中也是一种封装;将类函数定义和类声明放在不同的文件中也是一种封装。
二、构造函数和析构函数
1. 构造函数
专门用于构造新对象,将值赋给它们的数据成员,名称与类名相同。
// 声明
class className {
private:
int example;
public:
className(int m_example) {
example = m_example;
}
}
// 使用
className instance = className(10); // 显式
className instance(10); // 隐式
className *p = new className(10); // 与new结合动态分配内存
className instance = {10, 20}; // C++11列表初始化
className instance = 10; // 接收一个参数时允许赋值语法将对象初始化为一个值,易错建议少用
注:
- 构造函数中的参数名不能与类的成员名相同,否则会出现错误
- 与
new
结合的方式会导致所创建的对象没有名称,但可以使用指针来进行管理 - 无法使用对象来调用构造函数,因为调用构造函数之前对象未被创建
由于构造函数对于创建对象的重要性,因此有默认构造函数。默认构造函数是指面对下面这类情况时编译器将自动调用构造函数:
className instance;
只有当未定义任何构造函数的情况下系统才会自动生成这样的默认构造函数。
而开发者可以定义默认构造函数,下面举个例子:
// 1.自动生成的默认构造函数
className::className() {};
// 2.用户自定义的默认构造函数
className::className() {
...
};
// 3.用户定义的带默认参数的默认构造函数
className::className(int example = 10) {
...
};
// 4.用户定义的无默认参数的构造函数(普通构造函数)
className::className(int example) {
...
};
注:第四种情况不能进行如下定义:
className example; // 错误,未使用构造函数
若想进行上述定义,可以使用重载特性创建第一种情况的默认构造函数,但是一定要注意,不能同时定义第二种、第三种情况的默认构造函数,否则系统将不能确定你想使用哪一种来定义对象,导致错误。
2. 析构函数
对象过期时,析构函数完成对对象的清理工作,由于对象过期时通常不需要什么操作(除new创建的指针外),因此通常设置为空函数:
className :: ~classname() {};
析构函数的调用时间由编译器决定,静态存储类对象通常在程序结束时自动调用;自动存储类对象则通常是程序执行完代码块后自动调用;若是由new
创建的对象,则通常使用delete
释放内存时自动调用。
析构函数是类不可或缺的一份子,若用户没有定义它,则由编译器自动生成。
如果对象的作用域为整个主函数,那么在 dos 窗口运行环境中,窗口会在析构函数运行之前退出,因此看不到析构函数的输出。
3. 拷贝构造函数
顾名思义,就是用已经定义好的对象来初始化目标对象,举个栗子:
Line::Line(const Line &obj)
{
example = obj.example; // 拷贝值
}
Line ep; // 创建一个对象
// 以下调用均使用拷贝构造函数
Line a(ep); // 直接调用
Line b = ep; // 直接创建b,或先生成一个临时对象,然后再将内容赋给b
Line c = Line(ep); // 同b
Line *d = new Line(ep); // 先初始化一个匿名对象,并将其地址赋给d指针
由于拷贝构造函数的存在,因此需要注意构造函数与拷贝构造函数之间的关系,防止某些必要操作只在构造函数中而未写入拷贝构造函数,导致出现错误。此外,还应当特别注意指针与析构函数的关系,如果利用拷贝构造函数复制的对象的成员变量中存在指针,那么倘若其因作用域被销毁时,可能会导致被复制的对象数据出现错误。
因此如果类中包含使用 new 初始化的指针成员,应当定义一个拷贝构造函数,以复制指向的数据,而不是指针,这被称为深度复制。相对应的浅复制(成员复制) 仅复制所有成员,而不会深入挖掘指针指向的内容。
还有一种特殊情况叫做赋值运算符,即:
String a("example");
String b;
b = a; // 赋值运算符
// 特别注意
String c = a; // 拷贝构造函数,而不是赋值运算符
此处的对象 b 首先使用默认构造函数,然后再通过赋值运算符赋值为 a ,默认情况下赋值运算符采用浅复制,因此对于存在指针的情况需要重写它。
三、const 成员函数
现在看下面这种情况:
const className instance;
instance.show();
编译器将无法执行第二行,原因是不能保证show
函数不会对对象进行任何修改。
又因为show
函数是void
类型,因此用以前的语法方式不能确保该函数未const
类型,因此需要一种新的语法来对其进行声明:
// 函数声明
void show() const;
// 函数定义
void className::show() const {
...
}
四、this指针
当我们需要在一个对象中引用另一个对象时,可能会引发一下奇怪的问题,举个栗子:
// className中定义的比大小函数,需要引用一个对象,并且返回值也为对象
const className & className::compare(const className & example2) const {
if(flag < example2.flag) // 返回最大值
return example2;
else return ???; // 此时
}
此时可以发现,example2
在引用时拥有别名example2
,但是此时调用的是example1
的成员方法,因此不能返回example1
,原因是该方法并不知道example1
是什么东西。
因此有了this
指针,它的作用就是指向自己对象的指针。事实上类中所有的成员函数都具有该指针,包括构造函数与析构函数。
因此上述代码中的返回值应该是:
return *this;
五、对象数组
与不同数据类型声明的方式相同,只不过需要多考虑一步构造函数:
// 不同构造函数定义对象数组
className example[3] = {
className(1),
className(2, "instance"),
className()
};
六、类作用域
类只是规定了构造对象的形式,因此不能在类中为变量赋默认值,可行的替代方法是使用枚举变量限定默认值:
class className {
private:
// 错误示例:
const int Months = 12;
double costs[Months];
// 正确示例:
enum {Months = 12};
double costs[Months];
};
C++提供另一种定义常量的方式:关键字static
:
class className {
private:
static const int Months = 12;
double costs[Months];
};
该静态类型变量不存储在对象中,而是与其他静态类型存储在一起,被所有对象共享。
枚举类型也有自己的问题,举个栗子:
// 声明:
enum Apple {small, large};
enum Banana {small, large};
// 定义:
Apple apple = small;
Banana banana = large;
由于两个枚举类型具有相同的作用域,此时系统将无法区分所定义的枚举类型究竟是哪一个类型的限定值,因此上述代码将不能通过编译,解决方法如下:
// 声明:
enum class Apple {small, large};
enum class Banana {small, large};
// 定义:
Apple apple = Apple::small;
Banana banana = Banana::large;
但是常规的枚举支持自动转换,而作用域内枚举则只支持显示转换:
enum egg {small, large};
enum class banana {small, large};
egg e = small;
banana b = banana::small;
int ep = e; // allow
int ep = b; // not allow
int ep = int(b); // allow