C++类与对象
1. 问题
C++语言只提供了整数、浮点数、bool、字符等基本类型。如何处理系统没有内置的类型?比如复数?时间?日期?坐标?传统C语言的做法是使用结构体,比如:
1 struct complex {
2 double real;
3 double imag;
4 };
5 complex add(complex a, complex b);
6 complex substract(complex a, complex b);
7 complex multiply(complex a, complex b);
8 void display(complex a);
9 void set(complex *c, double r, double i);
10
11 int main() {
12 complex x, y;
13 set(&x, 10.0, 20.0);
14 set(&y, 1.0, -2.0);
15 complex z = add(x, y);
16 display(z);
17 }
另一个例子:
1 struct clock {
2 int second;
3 int minute;
4 int hour;
5 };
6 void set_time( clock *c, int h, int m, int s) {
7 c->second = s;
8 c->minute = m;
9 c->hour = h;
10 }
11 void show_time(const clock *c) {
12 cout << c->hour << c->minute << c->second;
13 }
14 void inc_second(clock *c) {
15 c->second ++;
16 if(c->second == 60) {
17 c->minute ++;
18 c->second = 0;
19 if(c->minute == 60) {
20 c->hour ++;
21 c->minute = 0;
22 if(c->hour == 24)
23 c->hour = 0;
24 }
25 }
26 }
27
28 int main() {
29 clock c;
30 set_time(&c, 10, 10, 30);
31 inc_second(&c);
32 show_time(&c);
33 }
再一个例子:
1 #define MALE true
2 #define FEMALE false
3 struct student{
4 char num[20];
5 char name[10];
6 bool gender;
7 };
8 void set(student &s, char id[], char n[], bool g){
9 strcpy(s.num, id);
10 strcpy(s.name, n);
11 s.gender = g;
12 }
13 void display(student &s) {
14 cout << s.num << s.name << (s.gender?"male":"female");
15 }
16 int main() {
17 student stud1, stud2;
18 set(stud1, "110101", "Rose", FEMALE);
19 set(stud2, "110102", "JACK", MALE);
20 display(stud1);
21 display(stud2);
22 }
2. C++的类
C语言中,表示数据的结构体和操作这些结构体的函数是分开的。或者说数据结构和操作它的算法是分开的。这样数据和操作之间的关系不是很清晰。C++的做法是在C语言的基础上更进一步,将数据与操作这组数据的函数结合在一起,构成类(class)
C++中类的定义可以用struct或者用class。函数与数据结合在一起,逻辑关系更加明确。定义在类中的函数又被称为方法、成员函数。成员函数可以直接访问类(结构体)中的数据成员。C++类中函数的定义,函数体可以直接写在类的内部,写在头文件中:
1 struct clock {
2 int hour, minute, second;
3 void set_time(int h, int m, int s) {
4 hour = h;
5 minute = m;
6 second = s;
7 }
8 void show_time() {
9 cout << hour << minute << second;
10 }
11 void inc_second(clock *c) {
12 second ++;
13 if(second == 60) {
14 minute ++;
15 second = 0;
16 if(minute == 60) {
17 hour ++;
18 minute = 0;
19 if(hour == 24)
20 hour = 0;
21 }
22 }
23 }
24 };
25
26 int main() {
27 clock c1, c2;
28 c1.set_time(10, 10, 30);
29 c2.set_time(18, 00, 00);
30 c1.show_time();
31 c2.show_time();
32 }
也可以分开定义,比如:
1 //clock.h头文件:
2 struct clock {
3 int hour, minute, second;
4 void set_time(int h, int m, int s);
5 void show_time();
6 void inc_second();
7 };
8
9 //clock.cpp源文件:
10 void clock::set_time(int h, int m, int s) {
11 hour = h;
12 minute = m;
13 second = s;
14 }
15 void clock::show_time() {
16 cout << hour << minute << second;
17 }
18 void clock::inc_second() {
19 second ++;
20 if(second == 60) {
21 minute ++;
22 second = 0;
23 if(minute == 60) {
24 hour ++;
25 minute = 0;
26 if(hour == 24)
27 hour = 0;
28 }
29 }
30 }
31
32
33
34 // main.cpp源文件
35 int main() {
36 clock c1, c2;
37 c1.set_time(10, 10, 30);
38 c2.set_time(18, 00, 00);
39 c1.show_time();
40 c2.show_time();
41 }
另一个例子:
1 const bool MALE = true;
2 const bool FEMALE = false;
3 struct student{
4 string num;
5 string name;
6 bool gender;
7 void set(string id, string n, bool g) {
8 num = id;
9 name = n;
10 gender = g;
11 }
12 void diplay() {
13 cout << num << name << (gender?"male":"female");
14 }
15 };
16 int main() {
17 student stud1, stud2;
18 stud1.set("110101", "Rose", FEMALE);
19 stud2.set("110102", "Jack", MALE);
20 stud1.display();
21 stud2.display();
22 }
对象的定义和访问方式类似于结构体
在堆空间分配和访问对象
定义对象数组
成员函数和普通函数一样可以内联。函数体写在类定义内部的成员函数,默认就是内联的。
类外定义函数体的成员函数,要定义成内联需要加inline关键字,并把函数体写在头文件中
成员函数也可以重载
成员函数也可以有缺省参数。缺省值写在声明中,而不是定义中。
3. 类的访问权限
在前面的clock类中,我们发现类中的数据成员hour、minute、second不需要被clock类的使用者直接访问。如果直接访问,还可能会带来副作用。在C++中增加了对类的成员的访问权限的控制,把成员分为public和private等。
public部分定义了类的外部接口,可供类的使用者调用。private部分隐藏了类的具体实现,由类的实现者实现,不需要使用者关心。这就是‘’‘封装’‘’。private和public为新增的关键字。
struct与class的访问权限的区别:struct默认public,class默认为private
另一个例子:
1 class student{
2 private:
3 string num;
4 string name;
5 bool gender;
6 public:
7 void display() {
8 cout << num << name << (gender?"male":"female");
9 }
10 void setnum(string n) {
11 num = n;
12 }
13 void setname(char n) {
14 name = n;
15 }
16 void setgender(bool g) {
17 gender = g;
18 }
19 };
20
21 int main() {
22 student stud1;
23 stud1.setname("jack");
24 stud1.setnum("05020001");
25 stud1.setgender(true);
26 stud1.display();
27 cout << stud1.gender; //error
28 }
并非所有数据成员都必须是private,并非所有成员函数都必须是公有的。
1 class clock {
2 private: //只能在类内访问
3 int hour, minute, second;
4 public: //可以在类外访问
5 void add(int s) {
6 for(int i = 0; i < s; i++)
7 inc();
8 }
9 private:
10 void inc() {
11 second++;
12 if(second == 60) {
13 second = 0;
14 minute ++;
15 if(minute == 60) {
16 minute = 0;
17 hour++;
18 if(hour == 24)
19 hour = 0;
20 }
21 }
22 }
23 };
4. 构造与析构
变量定以后,没有初始化就使用是错误的。比如:
定义变量的同时完成初始化(resource acquisition is initialization)是一种好的编程习惯,可以避免错误。对于普通变量可以这样:
而对于类变量,我们也希望可以完成类似的初始化,比如:
要这样做可以在类中定义一个特殊的函数——构造函数。它在对象被定义时就被自动调用,以确保对象能够初始化。
注意:构造函数是没有返回类型的,写成这样是语法错误:
在定义了这样的构造函数以后,不使用参数来创建对象就变成了语法错误:
有时希望对象不需要参数也可以初始化,初始化到一种默认的状态,则可以用没有参数的构造函数,称作默认构造函数(default constructor)。
1 class clock{
2 int hour, minute, second;
3 public:
4 clock() {
5 hour = minute = second = 0;
6 }
7 void show_time() {
8 cout << hour << minute << second;
9 }
10 };
11
12 int main() {
13 clock c1; // 初始化为0时0分0秒
14 c1.show_time();
15 clock c2(15, 30, 0) // 错误,没有这样的构造函数
16 clock c3(); // 定义函数而不是定义对象
17 }
注意,要调用默认构造函数创建对象,不要在后面添加空的(),否则会被认为是函数声明,而不是对象定义。
如果要以多种不同的方式初始化对象,可以对构造函数进行重载。构造函数也可以有缺省参数:
各个构造函数所需要参数不同。在定义对象时,会根据传递的参数来选择一个特定的构造函数初始化对象。
一个所有参数具有默认参数的构造函数也是默认构造函数,比如
构造函数和普通函数一样,也可以定义在类外:
1 class clock{
2 int hour, minute, second;
3 public:
4 clock(int h, int m, int s);
5 };
6 clock::clock( int h, int m, int s) {
7 hour = h;
8 minute = m;
9 second = s;
10 }
11
12 int main() {
13 clock sleep(22, 30, 0);
14 clock getup(6, 30, 30);
15 clock now = clock(9, 21, 20);
16 clock *pC = new clock(23, 30, 25); //构造函数会被自动调用
17 }
在用new动态分配对象的时候,构造函数也会被自动调用。如果是用malloc分配内存,构造函数不会被自动调用。这是new和malloc最重要的区别。
要定义对象数组,并且没有初始化,那么要求该类可以默认构造对象。比如:
如果类的构造函数需要参数,则需要对数组进行初始化:
1 class clock {
2 int hour, minute, second;
3 public:
4 clock(int h , int m, int s) {
5 hour = h;
6 minute = m;
7 second = s;
8 }
9 };
10 void f() {
11 clock cls[10] = {clock(0,0,0), clock(1,0,0), clock(2,0,0), clock(3,0,0), clock(4,0,0), clock(5,0,0), clock(6,0,0), clock(7,0,0), clock(8,0,0), clock(9,0,0), clock(10,0,0)};
12 clock *p = new clock[10]; // 错误,没有办法初始化
13 }
在前面写的构造函数中,我们对类的成员进行赋值,而不是初始化。在类的成员必须初始化而不能赋值的时候,这样做会带来问题。比如说类内有引用成员和const成员:
要对成员进行初始化,需要使用构造函数的初始化列表:
对于引用也要用同样的方式初始化。而对于普通变量也可以用初始化表进行初始化:
在初始化列表里面要用()而不是=给出初始值。初始化列表还可以用在类中内嵌对象的情况:
如果一个类中一个构造函数都没有写,编译器会自动生成一个默认构造函数。自动生成的构造函数可以调用内嵌类型的默认构造函数(如果有默认构造函数的话)。
对象被摧毁时,另一个特殊的成员函数也会自动被调用,这个函数称为称为析构函数,一般用来完成资源的释放工作。
析构函数的名字为类名前加~,没有返回类型和参数。析构函数不能重载。析构函数会被自动调用,例如:
1 class clock {
2 int hour, minute, second;
3 public:
4 ~clock() {
5 cout << "destruct:"<< hour << minute << second;
6 }
7 };
8 int main() {
9 clock d1(4, 11, 1);
10 if( true ){
11 clock *d2 = new clock(9, 9, 9);
12 clock d3(8, 10,0);
13 //…
14 } // d3 is destructed here
15 delete d2; // d2 is destructed here
16 clock d4;
17 } // d1,d4 is destructed here
18
析构函数一般用在需要资源释放的地方,比如:
new会自动调用构造函数,而malloc不能。同样delete会自动调用析构函数,而free不能。
5. 对象的拷贝构造
对象的复制分为两种情况:赋值和拷贝构造。拷贝构造是指创建一个新对象,创建的同时让新对象的值与另一个已存在的对象一模一样。赋值是复制一个对象的值给另一个已存在的对象,使两个对象的值一样。比如
拷贝构造的工作由一个特殊的构造函数拷贝构造函数完成。如果一个类中没有编写拷贝构造函数,编译器会自动生成一个拷贝构造函数,用于完成默认的拷贝构造的功能:用源对象的所有数据成员逐一拷贝构造目标对象的相应成员。我们可以自定义拷贝构造函数去改写默认的拷贝构造函数。比如:
1 class clock {
2 int hour, minute, second;
3 public:
4 clock(int h, int m, int s) {
5 hour = h;
6 minute = m;
7 second = s;
8 }
9 //这个拷贝构造函数实现的功能与编译器自动声成的相同,可以省略
10 clock(clock& c) {
11 hour = c.hour;
12 minute = c.minute;
13 second = c.second;
14 }
15 };
16
17 int main() {
18 clock c;
19 clock d(c); //拷贝构造d,其值与c一样。
20 }
何时需要自定义拷贝构造函数、析构函数?当涉及到内存管理时,一个类中有指针成员,指向动态分配的空间,那么通常需要写一组拷贝构造函数、析构函数和赋值操作符。例如:
1 class mystring{
2 char *s;
3 public:
4 mystring(char *str) {
5 s = new char[strlen(str)+1];
6 strcpy(s, str);
7 }
8 ~mystring() {
9 delete[] s;
10 }
11 mystring(mystring &str) {
12 s = new char[strlen(str.s)+1];
13 strcpy(s, str.s);
14 }
15 };
16 int main() {
17 mystring s("Hello");
18 mystring s2(s);
19 }
再例如:写一个类array,模拟数组的功能,元素是int类型,数组的大小可以是变量,增加检查越界的功能。即可以这样使用:
但是这个类在这样使用时存在问题:
拷贝构造的对象与原对象共享同一块空间,修改了一个对象,另一个对象也受影响。当一个对象释放时,另一个对象也不能使用了。当两个对象都被释放时,同一个空间释放了两次。解决办法是自定义拷贝构造函数:
1 class array {
2 private:
3 int *p;
4 int size;
5 public:
6 array(int s) {
7 size = s;
8 p = new int [ size ];
9 }
10 ~array() {
11 delete[ ] p;
12 }
13 int & at( int i) {
14 if( i < size)
15 return p[i];
16 else
17 return 0;
18 }
19 array( array &a) {
20 size = a.size;
21 p = new int [size];
22 for(int i = 0; i < size; i++)
23 p[i] = a.p[i];
24 }
25 };
6. 类的组合
构造复杂的对象的一种方法是组合。一个类可以使用另一个类的对象作为成员,比如:
1 class point{
2 private:
3 double x, y;
4 public:
5 //...
6 };
7
8 class line {
9 private:
10 point start, end;
11 public:
12 //...
13 };
14
15 class circle {
16 private:
17 point center;
18 double radius;
19 public:
20 //...
21 };
22
23 class rectangle {
24 private:
25 point p1, p2;
26 public:
27 //...
28 };
组合对象的构造
这个程序遇到编译错误。一是因为point类的x,y是私有的,在line类的构造函数中访问point的x,y是错误的。二是构造line中内含的两个point对象start,end需要参数,而在line的构造函数中没有给出这些参数。解决办法:使用构造函数初始化列表
1 class point{
2 private:
3 double x, y;
4 public:
5 point(double x0, double y0) {
6 x = x0;
7 y = y0;
8 cout << "point " << x << y << "initialized!" << endl;
9 }
10 };
11
12 class line {
13 private:
14 point start, end;
15 public:
16 line(double x0, double y0, double x1, double y1)
17 : start(x0, y0), end(x1, y1) {
18 cout << "line initializing" << endl;
19 }
20 };
21 int main() {
22 line l(1,2,3,4);
23 }
在line构造函数的初始化列表中,给start、end成员的构造函数传递参数完成它们的初始化。
如果内嵌的类有默认构造函数,则初始化列表可以省略。
1 class point{
2 private:
3 double x, y;
4 public:
5 point(double x0 = 0, double y0 = 0) {
6 x = x0;
7 y = y0;
8 cout << "point " << x << y << "initialized!" << endl;
9 }
10 };
11
12 class line {
13 private:
14 point start, end;
15 public:
16 line(double x0, double y0, double x1, double y1) {
17 cout << "line initializing" << endl;
18 }
19 };
20
21 int main() {
22 line l(1,2,3,4);
23 }
初始化表还能够解决其它一些类型的成员的初始化问题,比如const成员,引用成员等。比如
构造函数和析构函数执行的顺序
1 class point{
2 private:
3 double x, y;
4 public:
5 point(double a, double b) :x(a), y(b) {
6 cout << "construct point" << endl;
7 }
8 ~point() {
9 cout << "destruct point" << endl;
10 }
11 };
12 class line {
13 private:
14 point start, end;
15 public:
16 line(double x0, double y0, double x1, double y1)
17 : start(x0, y0) , end(x1, y1) {
18 cout << "construct line" << endl;
19 }
20 ~line() {
21 cout << "destruct line" << endl;
22 }
23 };
7. 静态成员
参见:C++:静态成员
8. 友元
参见:C++:友元
9. const对象
定义对象前面加const,表示对象不能改变
1 class clock{
2 int hour, minute, second;
3 public:
4 clock(int h, int m, int s)
5 : hour(h), minute(m), second(s)
6 {
7 }
8 void set_time(int h, int m, int s) {
9 hour = h;
10 minute = m;
11 second = s;
12 }
13 void show_time() {
14 cout << hour << minute << second;
15 }
16 };
17 int main() {
18 const int i = 10;
19 cout << i*10; // ok
20 i++; // error!
21 const clock t(10, 30, 20);
22 cout << t.second; // ok
23 t.second = 10; // error
24 t.set_time(10, 20, 10); // error!
25 t.show_time(); // error!
26 }
const对象不能调用普通的成员函数,即使这个函数并不修改数据成员。对于不修改数据成员的成员函数,可以在声明后面加const,表示这个成员函数不修改数据成员,这样的函数可以被const对象调用。
1 class clock{
2 int hour, minute, second;
3 public:
4 clock(int h, int m, int s)
5 : hour(h), minute(m), second(s)
6 {
7 }
8 void set_time(int h, int m, int s) {
9 hour = h;
10 minute = m;
11 second = s;
12 }
13 void show_time() const {
14 cout << hour << minute << second;
15 }
16 };
17 int main() {
18 const int i = 10;
19 cout << i*10; // ok
20 i++; // error!
21 const clock t(10, 30, 20);
22 cout << t.second; // ok
23 t.second = 10; // error
24 t.set_time(10, 20, 10); // error!
25 t.show_time(); // ok
26 }
const成员函数可以被const对象调用,也可以被普通对象调用。
常成员函数与非常成员函数可以重载
1 class array {
2 private:
3 int *p, size;
4 public:
5 array(int s) {
6 size = s;
7 p = new int [ size ];
8 }
9 ~array() { delete[ ] p; }
10 int & at( int i) {
11 if( i < size) return p[i]; else throw out_of_range();
12 }
13 int at( int i) const {
14 if( i < size) return p[i]; else throw out_of_range();
15 }
16 };
17 int main() {
18 array a(10);
19 a.at(5) = 5;
20 const array b(5);
21 b.at(5) = 5; // error
22 }
10. this指针
参见:C++:this指针