Coursera Cpp程序设计 week 2

第二周的内容比较多,主要是介绍类的一些重要概念,下面回顾下。

课程地址:

coursera:C++程序设计

https://www.coursera.org/learn/cpp-chengxu-sheji

中国大学MOOC:程序设计与算法(三)C++面向对象程序设计

程序设计与算法(三)C++面向对象程序设计

类和对象

类成员的可访问范围

在类的定义中,用下列访问范围关键字来说明类成员 可被访问的范围:

  • private: 私有成员,只能在成员函数内访问
  • public : 公有成员,可以在任何地方访问
  • protected: 保护成员,以后再说

设置私有成员的机制,叫“隐藏” ,“隐藏”的目的是强制对成员变量的访问一定要通过成员函数进行,那么以后成员变量的类型等属性修改后,只需要更改成员 函数即可。否则,所有直接访问成员变量的语句都需要修改。

构造函数

构造函数有如下特性

  • 名字与类名相同,可以有参数,不能有返回值(void也不行)
  • 作用是对对象进行初始化,如给成员变量赋初值
  • 如果定义类时没写构造函数,则编译器生成一个默认的无参数 的构造函数,默认构造函数无参数,不做任何操作
  • 如果定义了构造函数,则编译器不生成默认的无参数的构造函数
  • 对象生成时构造函数自动被调用。对象一旦生成,就再也不能在 其上执行构造函数
  • 一个类可以有多个构造函数

看一个具体的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Complex {
private :
double real, imag;
public:
Complex( double r, double i = 0);
};

Complex::Complex( double r, double i) {
real = r; imag = i;
}

Complex c1; // error, 缺少构造函数的参数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2,4), c2(3,5);
Complex * pc = new Complex(3,4);

此外,构造函数可以重载,看一个具体例子

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
class Complex {
private :
double real, imag;
public:
void Set( double r, double i );
Complex(double r, double i );
Complex (double r );
Complex (Complex c1, Complex c2);
};

Complex::Complex(double r, double i)
{
real = r; imag = i;
}

Complex::Complex(double r)
{
real = r; imag = 0;
}
Complex::Complex (Complex c1, Complex c2);
{
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
Complex c1(3) , c2 (1,0), c3(c1,c2);
// c1 = {3, 0}, c2 = {1, 0}, c3 = {4, 0};

复制构造函数

  • 只有一个参数,即对同类对象的引用。
  • 形如 X::X( X &)或X::X(const X &), 二者选一 后者能以常量对象作为参数。
  • 如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。

来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
class Complex {
public :
double real,imag;
Complex(){ }
Complex( const Complex & c ) {
real = c.real;
imag = c.imag;
cout << “Copy Constructor called”;
}
};
Complex c1;
Complex c2(c1);//调用自己定义的复制构造函数,输出 Copy Constructor called

注意如果定义的自己的复制构造函数, 则默认的复制构造函数不存在。

此外不允许有形如 X::X( X )的构造函数。

1
2
3
4
class CSample {
CSample( CSample c ) {
} //错,不允许这样的构造函数
};

来看下复制构造函数有哪些作用

复制构造函数起作用的三种情况

1)当用一个对象去初始化同类的另一个对象时。

1
2
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句

2)如果某函数有一个参数是类 A 的对象, 那么该函数被调用时,类A的复制构造函数将被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
public:
A() { };
A( A & a) {
cout << "Copy constructor called" <<endl;
}
};

void Func(A a1){ }
int main(){
A a2;
Func(a2);
return 0;
}

程序输出结果为: Copy constructor called

3) 如果函数的返回值是类A的对象时,则函数返回时, A的复制构造函数被调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A
{
public:
int v;
A(int n) { v = n; };
A( const A & a) {
v = a.v;
cout << "Copy constructor called" <<endl;
}
};

A Func() {
A b(4);
return b;
}

int main() {
cout << Func().v << endl;
return 0;
}

//输出结果:
//Copy constructor called
//4

值得注意的是,对象间赋值并不导致复制构造函数被调用,来看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CMyclass {
public:
int n;
CMyclass() {};
CMyclass( CMyclass & c) { n = 2 * c.n ; }
};
int main() {
CMyclass c1,c2;
c1.n = 5; c2 = c1; CMyclass c3(c1);
cout <<"c2.n=" << c2.n << ",";
cout <<"c3.n=" << c3.n << endl;
return 0;
}
//输出: c2.n=5,c3.n=10

类型转换构造函数

  • 定义转换构造函数的目的是实现类型的自动转换。
  • 只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数。
  • 当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Complex {
public:
double real, imag;
Complex( int i) {//类型转换构造函数
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r,double i) {real = r; imag = i; }
};

int main ()
{
Complex c1(7,8);
Complex c2 = 12;
c1 = 9; // 9被自动转换成一个临时Complex对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}

析构函数

  • 名字与类名相同,在前面加‘~’ , 没有参数和返回值, 个类最多只能有一个析构函数。
  • 析构函数对象消亡时即自动被调用。可以定义析构函数来在 对象消亡前做善后工作,比如释放分配的空间等。
  • 如果定义类时没写析构函数,则编译器生成缺省析构函数。 缺省析构函数什么也不做。
  • 如果定义了析构函数,则编译器不生成缺省析构函数。
析构函数的调用的时机

1) 对象数组生命期结束时,对象数组的每个元素的析构函数都会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Ctest {
public:
~Ctest() { cout<< "destructor called" << endl; }
};
int main () {
Ctest array[2];
cout << "End Main" << endl;
return 0;
}

//输出:
//End Main
//destructor called
//destructor called

2) delete运算导致析构函数的调用,若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对 象(调用一次析构函数)。

1
2
3
4
5
6
Ctest * pTest;
pTest = new Ctest; //构造函数调用
delete pTest; //析构函数调用
---------------------------------------------------------
pTest = new Ctest[3]; //构造函数调用3次
delete [] pTest; //析构函数调用3次

3)析构函数在对象作为函数返回值返回后被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CMyclass {
public:
~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj ) { //参数对象消亡也会导致析构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main(){
obj = fun(obj); //函数调用的返回值(临时对象)被用过后,该临时对象析构函数被调用
return 0;
}

//输出:
//destructor
//destructor
//destructor

this 指针

this 指针的作用

非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针,来看下面的例子

1
2


来看一个比较容易混淆的例子

1
2
3
4
5
6
7
8
9
10
11
class A
{
int i;
public:
void Hello() { cout << "hello" << endl; }
}; //相当于 void Hello(A * this ) { cout << "hello" << endl; }
int main()
{
A * p = NULL;
p->Hello();
} // 输出: hello
this指针和静态成员函数

静态成员函数中不能使用this指针,因为静态成员函数并不具体作用与某个对象。因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数。

静态成员

静态成员:在说明前面加了static关键字的成员。

例子:

1
2
3
4
5
6
7
8
9
10
11
class CRectangle
{
private:
int w, h;
static int nTotalArea; //静态成员变量
static int nTotalNumber;
public:
CRectangle(int w_,int h_);
~CRectangle();
static void PrintTotal(); //静态成员函数
};
静态成员的性质

普通成员变量每个对象有各自的一份,而静态成员变 量一共就一份,为所有对象共享,sizeof 运算符不会计算静态成员变量,来看如下的例子:

1
2
3
4
5
class CMyclass {
int n;
static int s;
};
//sizeof( CMyclass ) = 4

普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象,从而静态成员不需要通过对象就能访问,来看几种访问方法:

  • 1) 类名::成员名 CRectangle::PrintTotal();
  • 2) 对象名.成员名 CRectangle r; r.PrintTotal();
  • 3) 指针->成员名 CRectangle * p = &r; p->PrintTotal();
  • 4) 引用.成员名 CRectangle & ref = r; int n = ref.nTotalNumber;

和普通成员函数相比,多了类名::成员名这种访问方法。

注意事项
  • 必须在定义类的文件中对静态成员变量进行一次说明或初始化。例如 int CRectangle::nTotalNumber = 0;
  • 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。

成员对象和封闭类

有成员对象的类叫封闭enclosing)类,来看如下例子。

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
class CTyre //轮胎类
{
private:
int radius; //半径
int width; //宽度
public:
CTyre(int r,int w):radius(r),width(w) { }
};

class CEngine //引擎类
{
};

class CCar { //汽车类
private:
int price; //价格
CTyre tyre;
CEngine engine;
public:
CCar(int p,int tr,int tw );
};

CCar::CCar(int p,int tr,int w):price(p),tyre(tr, w)
{
};

int main()
{
CCar car(20000,17,225);
return 0;
}

上例中,如果CCar类不定义构造函数,则下面的语句会编译出错:

1
CCar car;

因为编译器不明白 car.tyre该如何初始化。 car.engine 的初始化没问题,用默认构造函数即可。任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。具体的做法就是:通过封闭类的构造函数的初始化列表。

封闭类构造函数和析构函数的执行顺序
  • 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执 行封闭类的构造函数。
  • 对象成员的构造函数调用次序和对象成员在类中的说明次序一致 ,与它们在成员初始化列表中出现的次序无关。
  • 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行 成员对象的析构函数。次序和构造函数的调用次序相反。

来看一个例子:

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
class CTyre {
public:
CTyre() { cout << "CTyre contructor" << endl; }
~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
public:
CEngine() { cout << "CEngine contructor" << endl; }
~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar {
private:
CEngine engine;
CTyre tyre;
public:
CCar( ) { cout << “CCar contructor” << endl; }
~CCar() { cout << "CCar destructor" << endl; }
};

int main(){
CCar car;
return 0;
}

//输出结果
//CEngine contructor
//CTyre contructor
//CCar contructor
//CCar destructor
//CTyre destructor
//CEngine destructor
封闭类的复制构造函数

封闭类的对象,如果是用默认复制构造函数初始化的,那么它里面包含的成员对象,也会用复制构造函数初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A
{
public:
A() { cout << "default" << endl; }
A(A & a) { cout << "copy" << endl;}
};
class B { A a; };

int main()
{
B b1,b2(b1);
return 0;
}

//输出:
//default
//Copy

输出结果说明b2.a是用类A的复制构造函数初始化的。而且调用复制构造函数时的实参就是b1.a。

友元

友元分为友元函数和友元类两种,下面分别介绍。

友元函数
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
class CCar ; //提前声明 CCar类,以便后面的CDriver类使用
class CDriver
{
public:
void ModifyCar( CCar * pCar) ; //改装汽车
};

class CCar
{
private:
int price;
friend int MostExpensiveCar( CCar cars[], int total); //声明友元
friend void CDriver::ModifyCar(CCar * pCar); //声明友元
};

void CDriver::ModifyCar( CCar * pCar)
{
pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar( CCar cars[],int total)
//求最贵汽车的价格
{
int tmpMax = -1;
for( int i = 0;i < total; ++i )
if( cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
友元类

如果A是B的友元类,那么A的成员函数可以访问B的私有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CCar
{
private:
int price;
friend class CDriver; //声明CDriver为友元类
};
class CDriver
{
public:
CCar myCar;
void ModifyCar() {//改装汽车
myCar.price += 1000;
//因CDriver是CCar的友元类,
//故此处可以访问其私有成员
}
};

注意友元类之间的关系不能传递,不能继承。

常量成员函数

如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加const关键字,来看一个例子。

1
2
3
4
5
6
7
8
9
class Sample {
private :
int value;
public:
Sample() { }
void SetValue() { }
};
const Sample Obj; // 常量对象
Obj.SetValue (); //错误。常量对象只能使用构造函数、析构函数和有const说明的函数(常量方法)

在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数,常量成员函数内部不能改变属性的值,也不能调用非常量成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Sample {
private :
int value;
public:
void func() { };
Sample() { }
void SetValue() const {
value = 0; // wrong
func(); //wrong
}
};
const Sample Obj;
Obj.SetValue (); //常量对象上可以使用常量成员函数

在定义常量成员函数和声明常量成员函数时都应该使用const关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Sample {
private :
int value;
public:
void PrintValue() const;
};
void Sample::PrintValue() const { //此处不使用const会
//导致编译出错
cout << value;
}
void Print(const Sample & o) {
o.PrintValue(); //若 PrintValue非const则编译错
}

int main(){
return 0;
}

注意,两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。