文章目录
1 定义类
1.1 声明类
类也要先声明后使用:不管声明的内容是否相同,声明同一个名字的两个类是错误的,类是具有唯一标识符的实体;在类中声明的任何成员不能使用 extern
、auto
和 register
关键字进行修饰;不能在类声明中对数据成员使用表达式进行初始化。
声明类的形式如下:
class 类名 {
private:
私有数据和函数
public:
公有数据和函数
protected:
保护数据和函数
}
class Point {
private:
double x, y;
public:
void setXY(int a, int b);
void move(int a, int b);
void display();
int getX();
int getY();
};
如果没有使用权限修饰符修饰数据成员和成员函数,默认声明为 private
权限。
1.2 定义成员函数
定义成员函数形式如下:
返回类型 类名::成员函数名(参数列表) {
// 函数实现
}
void Point::setXY(int a, int b) {
x = a;
y = b;
}
void Point::move(int a, int b) {
x = x + a;
y = y + b;
}
void Point::display() {
cout << x << "," << y << endl;
}
int Point::getX() {
return x;
}
int Point::getY() {
return y;
}
"::"
是作用域运算符,"类名"是成员函数所属类的名字,"::"
用于表明其后的成员函数是属于这个特定的类。注意:该成员函数在类中权限修饰为public时才需要定义类的成员函数实现。
如果在声明类的同时,在类体内给出成员函数的定义,则默认为内联函数。
class Point {
private:
double x, y;
public:
// 在类中定义,为内联函数
int getX() {
return x;
}
};
1.3 数据成员的赋值
不能在类体内给数据成员赋值,在类体外面就更不允许了。下面方法是错误的:
class Point {
int x = 25, y = 56; // 错误
};
赋值和初始化是两个不同的概念。赋初值是在有了对象A之后,对象A调用自己的成员函数setXY()实现赋值操作;初始化是使用与Point同名的构造函数Point(int, int)实现的。
2 使用类对象
对象和引用都使用运算符 "."
访问对象成员,指针则使用 "->"
运算符。
Point A, B;
Point* p = &A;
Point& R = B;
A.setXY(25, 88); // 对象调用成员函数
p->display(); // 指针调用成员函数
R.display(); // 引用调用成员函数
3 构造函数
3.1 默认构造函数
当没有为一个类定义任何构造函数的情况下,C++编译器总要自动建立一个无参构造函数。
需要特别强调的是:一旦程序定义了自己的构造函数,系统就不再提供默认构造函数。如果程序员没有再定义一个无参构造函数,但有声明了一个没有初始化的对象,则会导致编译错误。即:
class Point {
private:
double x, y;
public:
Point(int a, int b);
};
main中声明类:
Point a; // 错误,因为有了自定义的构造函数,系统不提供无参构造函数,编译错误
3.2 构造函数的定义和使用方法
class Point {
private:
double x, y;
public:
Point();
Point(int a, int b);
};
Point::Point() {} // 定义无参构造函数
Point::Point(int a, int b):x(a),y(b) {} // 定义自定义构造函数
以上类操作代码如下:
#include <iostream>
using namespace std;
class Point {
private:
double x, y;
public:
Point();
Point(int a, int b);
void setXY(int a, int b);
void moveXY(int a, int b);
double getX() {
return x;
}
double getY() {
return y;
}
void display() {
cout << x << "," << y << endl;
}
};
// 定义构造函数并初始化数据成员,:x(a),y(b)表示同时初始化数据成员,等价于x = a; y = b;
Point::Point(int a, int b):x(a), y(b) {
cout << "construct" << a << "," << b << endl;
}
// 和上面的定义构造函数初始化是等价的
//Point::Point(int a, int b) {
// x = a;
// y = b;
//}
Point::Point() {}
// 定义成员函数,该函数可以在类中定义为内联函数,这里仅演示
void Point::setXY(int a, int b) {
x = a;
y = b;
}
// 可以使用内联函数
void Point::moveXY(int a, int b) {
x += a;
y += b;
}
int main() {
Point a{}; // 无参构造初始化
Point b(10, 30); // 自定义构造初始化
Point* p = &a;
Point& r = a;
a.setXY(10, 20);
a.display();
a.moveXY(10, 20);
b.display();
p->display(); // 指针调用
r.display(); // 引用调用
b = a; // 将对象a的数据给b
b.display();
return 0;
}
3.3 构造函数和运算符new
运算符 new
用于建立生存期可控的对象,new返回这个对象的指针。
当使用new建立一个动态对象时,new首先分配足以保存Point类的一个对象所需要的内存,然后自动调用构造函数来初始化这块内存,再返回这个动态对象的地址。
使用new建立的动态对象只能用delete删除,以便释放所占内存。
void main() {
Point* ptr1 = new Point;
Point* ptr2 = new Point(5, 7);
delete ptr1;
delete ptr2;
}
3.4 构造函数的默认参数
如果程序定义自己的有参数构造函数,又想使用无参构造函数,解决的方法是将相应的构造函数全部使用默认参数设计。
class Point {
private:
double x, y;
public:
Point(int = 0, int = 0); // 自定义构造函数,又能使用无参构造,不需要给出参数名
};
Point::Point(int a, int b):x(a),y(b) {
cout << "initializing" << a << "," << b << endl;
}
3.5 复制构造函数
引用在类中一个很重要的用途是用在复制构造函数中这是一类特殊而且重要的函数,通常用于使用已有的对象来建立一个新对象。
在通常情况下,编译器建立一个默认复制构造函数,默认复制构造函数采用拷贝方式使用已有的对象来建立新对象,所以又直译为拷贝构造函数。
Point::Point(const Point&); // 复制构造函数,如果自己自定义了复制构造函数,则编译器不会再自动创建默认的复制构造函数
Point::Point(const Point& t) {
x = t.x; // 引用对象t访问了私有成员x和y
y = t.y;
}
构造函数 Point(const Point&)
是复制构造函数。
注意,在这个函数的实现中,它访问了对象的私有成员,这是允许的。在C++中,在一个类中定义的成员函数可以访问该类任何对象的私有成员。这个成员函数具有特殊的作用:在使用该类的一个对象初始化该类的另一个对象时,调用这个函数:
Point obj1(25, 52);
Point obj2(obj1); // 使用obj1的数据成员初始化obj2,即obj2的数据成员为25和52
使用obj1来初始化obj2,使obj2的数据成员与obj1的数据成员具有相等的值。复制构造函数必须使用对象的引用作为形式参数。
class Point {
private:
double x, y;
public:
Point(); // 无参构造函数原型声明
Point(const Point&); // 复制构造函数原型声明
void setXY(int a, int b) {
x = a;
y = b;
}
void display() {
cout << a << "," << b << endl;
}
};
// 定义无参构造
Point::Point() {}
// 定义复制构造
Point::Point(const Point& t) {
x = t.x;
y = t.y;
}
int main() {
Point a{};
a.setXY(10, 20);
a.display();
// 使用复制构造
Point b(a);
b.display();
return 0;
}
4 析构函数
在对象消失时,应使用析构函数释放由构造函数分配的内存。
为了与构造函数区分,在析构函数的前面加上一个 "~"
号。在定义析构函数时,不能指定任何返回类型,即使void返回类型也不行。析构函数也不能指定参数,但是可以显式地说明参数为void,如 A::~A(void)
。从函数重载角度分析,一个类也只能定义一个析构函数且不能指明参数,以便编译系统自动调用。
class Point {
private:
double x, y;
public:
~Point();
// ~Point(void);
};
// 如果没有定义析构函数,C++编译器会自己产生一个函数体为空的默认析构函数Point::~Point(){}
Point::~Point() {
cout << "destructor" << endl;
}
int main() {
auto* p = new Point; // new创建对象,先分配内存,再调用构造函数
delete p; // 对象销毁,自动调用析构函数,先调用析构函数,再释放内存,与new操作相反
Point* ptr = new Point[2];
delete[] ptr;
return 0;
}
当对象生存期结束时,程序为这个对象调用析构函数,然后回收这个对象占用的内存。全局对象和静态对象的析构函数在程序运行结束之前调用。
类的对象数组的每个元素调用一次析构函数。全局对象数组的析构函数在程序结束之前被调用。
5 this指针
Point对象a在执行成员函数a.setXY(25, 55)时,成员函数setXY(int, int)有一个隐藏参数,名为this指针。
// 成员函数this指针指向对象a
void Point::setXY(int a, int b, (Point*)this) {
this->x = a;
this->y = b;
}
C++规定,当一个成员函数被调用时,系统自动向它传递一个隐含的参数,该参数是一个指向调用该函数的对象的指针,从而使成员函数知道该对哪个对象进行操作。如果在定义setXY()函数时使用this指针,也不要给出隐含参数。除非特殊需要,一般情况下都省略符号 "this->"
,而让系统进行默认设置。
6 类和对象的性质
6.1 对象的性质
同一类的对象之间可以相互赋值。
Point A, B;
A.setXY(25, 55);
B = A; // A对象赋值给B对象
6.2 可使用对象数组。
Point A[3];
6.2 可使用指向对象的指针,使用取地址运算符&将一个对象的地址置于该指针中。
Point* p = &A;
p->display();
指向对象的指针不能取数据成员的地址,也不能取成员函数的地址。
6.4 对象可以用作函数参数
-
如果参数传递采用传对象值的方式,在被调用函数中对形参所作的改变不影响调用函数中作为实参的对象。如果传的是对象的引用(传地址),当参数对象被修改时,相应的实参对象也将被修改。
-
如果传的是对象的地址值,可以使用对象指针作为函数参数,这时也可以达到传引用的效果。C++推荐使用引用作为参数传递。为了避免被调用函数修改原来对象的数据成员,可以使用const修饰符。
6.3
// 使用对象为参数和引用为参数,系统会无法区分两个函数
void print(Point a) {
a.display();
}
void print(Point& a) {
a.display();
}
void print(Point* a) {
a->display();
}
注意:函数重载不能同时采用如上3中同名函数,因为使用参数为对象和对象引用时,编译系统无法区别这两个函数,重载print只能选择其中的一种。
7 类的性质
7.1 使用类的权限
-
类本身的成员函数可以使用类的所有成员(私有和公有函数)
-
类的对象只能访问公有成员函数,例如输出x只能使用A.getX(),不能使用A.x
-
其他函数不能使用类的私有成员,也不能使用公有成员函数,它们只能通过类的对象使用类的公有成员函数
-
虽然一个类可以包含另外一个类的对象,但这个类也只能通过被包含类的对象使用那个类的成员函数,通过成员函数使用数据成员,例如Loc.set(x, y)
7.2 不完全的类声明
类不是内存中的物理实体,只有当使用类产生对象时,才进行内存分配,这种对象建立的过程称为实例化。应当注意的是:类必须在其成员使用之前进行声明。
不完全声明的类不能实例化,否则会出现编译错误;不完全声明仅用于类和结构,企图存取没有完全声明的类成员,也会引起编译错误。
- 使用struct关键字设计类时,struct与class相反,struct的默认控制权限是public。一般建议不用struct设计类,提倡用struct设计只包含数据的结构,然后用这种结构作为类的数据成员。
8 面向对象编程的文件规范
一般要求将类的声明放在头文件中,非常简单的成员函数可以在声明中定义,实现放在.cpp文件中。在.cpp文件中,将头文件包含进去。
对规模较大的类,一般应该为每个类设立一个头文件和一个实现文件。但函数模板和类模板比较特殊,如果使用头文件说明,则同时完成它们的定义。为了避免重复包含头文件,可使用条件编译的方法。
- 编译指令
C++的源程序可包含各种编译指令,以指示编译器对源代码进行编译之前先对其进行预处理。所有的编译指令都以#开始,每条指令单独占用一行,同一行不能有其他编译指令和C++语句(注释除外)。
8.1 嵌入指令
#include <\user\prog.h>
注意:由于编译指令不是C++的一部分,因此,在这里表示反斜杠时只使用一个反斜杠。
8.2 宏定义
#define 宏名 替换正文
宏定义由新行结束,而不以分号结束。当替换正文要书写在多行上时,除最后一行之外,每行的行尾要加上一个反斜线,表示宏定义继续到下一行。
#define MAX(a,b) ((a)>(b)? \
(a):(b)
在程序的一个地方定义的宏名,如果不想使其影响到程序的其他地方,可以在不再使用时用#undef删除。
8.3 条件编译指令
条件编译指令是 #if
、#else
、#elif
和 #endif
。
编译指令#if用于控制编译器对源程序的某部分有选择地进行编译。该部分从#if开始,到#endif结束。如果#if后的常量表达值为真,则编译这部分,否则就不编译该部分,这时,这部分代码相当于被从源文件中删除。
编译指令#else在#if测试失效的情况下建立另外一种选择。
#error 出错信息
当遇到#error指令时,编译器显示其后面的“出错信息”,并中止对程序的编译。
8.4 defined操作符
关键字 defined
不是指令,而是一个预处理操作符。用于判定一个标识符是否已经被#define定义。
#include <iostream>
using namespace std;
#define DEBUG
void main() {
int i = 555;
#if defined(DEBUG)
cout << "DEBUG:value of i is" << i;
#endif
}
条件编译指令#ifdef和#ifndef用于测试其后的标识符是否被#define定义。 C++语言提供#ifndef和#ifdef只是为兼容C语言的程序,今后可能被淘汰,因此鼓励程序员使用defined进行标识符的测试。使用defined而不使用#ifdef或#ifndef测试一个标识符是否被定义的好处在于,程序员可以用逻辑运算组成复杂的测试表达式。
defined(LARGE) && !defined(SMALL) || !defined(TINY)