在前面的一章,我们介绍了C++语言面向对象编程的类的一些基本定义和方法,本章我们希望通过定义一些特殊的成员变量和成员函数来进一步丰满我们对于对象的认知。

本章主要内容:静态成员变量和静态成员函数;常量对象和常量成员函数;封闭类;成员对象;const成员和引用成员;友元;this指针。

static:静态成员变量和静态成员函数

static关键字作用在一个变量前,意味着这个变量是一个静态的变量,意味着它一旦生成,除非特别声明,直到程序结束才会消亡,就像第一章中最后举例中的所看到的,即使Func已经结束,但是其中定义的静态变量仍然没有消亡。对于一个类,当他的成员变量和成员变量前加上了static的声明,则称为静态成员变量和静态成员函数。

对于普通的成员变量,每一个对象各自有一份,而静态成员变量只有一份,它将被所有同类的对象共享。普通成员函数必须作用在一个具体的对象上,因为这样的函数除去我们定义的参数表外,还会自动加上作用的对象的指针(后面我们会介绍这就是this指针,类似于python中的self),而对于一个静态成员函数并不具体作用在某一个对象上,直接可以通过类名进行调用。

当我们想要访问一个静态成员变量,你当然可以继续使用对象名.成员名的方法进行,但由于其并不依赖于任何具体的对象,所以还可以通过类名::成员名的方法进行访问。值得注意的是,即便类没有任何已经生成和定义的对象时,其静态成员变量也可以被访问。静态成员变量本质上就是全局变量(但需要注意,写在private中的静态成员变量仍然无法被在类外被访问,这是为了保证安全性而必要的),所以在一个类中,不会将静态成员变量的内存计算在内,静态成员函数在本质上也就是全局函数,所以它不能访问任何具体类型中的成员变量,否则编译就会报错。

使用静态成员变量和函数,其实就是为了是程序更有整体的特性,例如我们考虑一个可以随时获取全部矩形面积和数量的类,这时我们就可以用两个静态成员变量来进行记录了:

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
#include<iostream>
using namespace std;
class CRectangle{
private:
int w,h;
static int totalArea,totalNumber;
public:
CRectangle(int w_,int h_):w(w_),h(h_){
totalArea+=w*h;
totalNumber++;
}
CRectangle(const CRectangle& r):w(r.w),h(r.h){
totalArea+=w*h;
totalNumber++;
}
~CRectangle(){
totalArea-=w*h;
totalNumber--;
}
static void PrintTotal(){
cout<<totalNumber<<','<<totalArea<<endl;
}
};
int CRectangle::totalNumber=0;//必须在定义类的文件中对静态成员变量进行声明或初始化
int CRectangle::totalArea=0;//你也可以把这个=0删掉,不会报错,但是把这句完全注释掉就会报错
int main(){
CRectangle r1(3,3),r2(2,2);
// cout<<CRectangle::totalNumber; //这样写就会报错,静态变量在私有中也是不能被访问的
CRectangle::PrintTotal();//静态函数不依赖于具体对象就可以调用
cout<<"-------------------"<<endl;
r1.PrintTotal();//当然你非要依赖也不是不行
cout<<"-------------------"<<endl;
CRectangle* pr=new CRectangle(r1);//别忘了复制构造函数也要修改
pr->PrintTotal();//当然这样也可以访问
cout<<"-------------------"<<endl;
delete pr;//检验析构函数是否正确
CRectangle::PrintTotal();
return 0;
}

输出结果为,可以看出基本实现了我们需要的功能:

1
2
3
4
5
6
7
2,13
-------------------
2,13
-------------------
3,22
-------------------
2,13

最后还是要着重提醒一下,静态成员函数中是不能出现任何具体对象的普通成员变量的,例如在PrintTotal()函数中,不能出现wh这两个普通成员变量,否则就会报错。

常量对象和常量成员函数

在前面将复制构造函数的时候,我们说过写一个以const class& name为参数的函数是最好的,这是因为在本函数中,我们会认为name这个对象的引用是一个常量,任何想要修改这个引用值的语句都会报错,为了安全性这是必要的。

如果我们希望某个对象的值初始化后就不再发生任何变化,那么在定义时,我们可以在它前面加上const关键字,使之成为一个常量对象。由于普通成员函数在执行过程中有可能会修改对象的值,所以不能通过常量对象调用普通成员函数,这样显然更加安全。作为补偿,可以通过常量对象调用常量成员函数,所谓常量成员函数,就是在定义时加上const关键字的成员函数(加的位置和以往有点不一样),请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
class Xisense{
public:
int u;
void GetValue() const;
Xisense(int i=0):u(i){} //实测不写这行会报错,常量的类中u不能没有值
};
void Xisense::GetValue() const{
cout<<u<<endl;
}
int main(){
const Xisense x;//常量对象x可以调用常量成员函数
x.GetValue();
return 0;
}

常量对象上可以执行常量成员函数。常量成员函数内部也不允许调用同类的其他非常量成员函数(静态成员函数除外)。注意两个函数的名字和参数表都相同时,但是一个时const,一个不是,它们也算是重载,就像两个复制构造函数。对于一个成员函数没有修改成员变量的值,没有调用非常量成员函数,那我们就不妨将其写成常量成员函数

不禁让我想到了oj上的一道题:

程序填空,是输出为2 \n 1 \n 1 \n 0

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
#include <iostream>
using namespace std;
class A
{
static int num;
public:
A(){num+=1;}
void func()
{
cout<< num <<endl;
}
// 在此处补充你的代码
};

int A::num=1;

int main()
{
A a1;
const A a2 = a1;
A & a3 = a1;
const A & a4 = a1;

a1.func();
a2.func();
a3.func();
a4.func();

return 0;
}

如果你忘记了除了参数表重载以外,还可以进行自身的const重载,那就不好做了QWQ,给出答案是:

1
2
3
4
void func() const{
num--;
cout<< num <<endl;
}

成员对象和封闭类

一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类就称为封闭类(enclosed class)。

初始化和消亡

使用初始化列表对封闭类进行构造是一种十分直观的初始化方法,在第一章中,我们介绍了只有普通成员变量时的初始化列表,在存在成员对象的封闭类中,我们应该在列表中指定使用哪个构造函数对成员对象进行初始化。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
class CTyre{
int radius,width;
public:
CTyre(int r,int w):radius(r),width(w){}//使用的是这种
CTyre():radius(0),width(0){}
};
class CCar{
int price;
CTyre tyre;
public:
CCar(int r,int w,int p):price(p),tyre(r,w){}//例如这里我们需要知道使用何种构造函数初始化tyre
};

如果你不去声明成员对象是如何进行初始化的,按照默认将执行无参构造函数,如果恰好这个成员对象的类没有无参构造函数,那么编译就会报错,所以这是值得注意的,尽可能的指名构造函数是一个好习惯。

封闭类对象生成时,将会先执行所有成员对象的构造函数,然后才会去执行封闭类自己的构造函数。成员对象构造顺序和定义时的次序一致,与列表中的次序无关(应该不会有人在意这种事吧?)。当封闭类消亡时,先执行封闭类自己的析构函数,然后再执行成员对象的析构函数

封闭类的复制构造函数

封闭类的对象,如果采用默认的复制构造函数进行初始化,那么它包含的成员对象也会使用复制构造函数进行初始化。可以举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
class A{
public:
A(){cout<<"default"<<endl;}
A(const A& a){cout<<"copy"<<endl;}
};
class B{
A a;
};
int main(){
B b1,b2(b1);
return 0;
}
//输出:
//default
//copy

const成员和引用成员

这两类成员变量必须在构造函数的初始化列表中进行初始化。常量型成员变量一旦初始化就不能再改变了。例如:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
int f;
class Xisense{
const int num;
int& ref;
int value;
public:
Xisense(int n):num(n),ref(f),value(4){}
};

暂时不知道有啥用,保留一下QWQ

友元

私有成员只能从类中访问,如果想在类外进行访问,就只能通过成员函数提供的接口来实现,这有时过于麻烦了。C++中友元的概念很好的解决了这个问题,相当于在类中做了一个白名单,这些是朋友,可以对我进行访问(咋感觉怪怪的QAQ)。友元在具体使用时可以分为友元函数和友元类两种。

友元函数

在定义一个类时,可以声明一些函数为“友元”,这些函数可以是全局函数,也可以是其他类的成员函数,这样这些函数就成为了该类的友元函数,在友元函数内部就可以访问该类的对象的私有成员了。声明友元的写法是:

1
2
friend 返回值类型 函数名(参数表);                   //全局函数声明为友元
friend 返回值类型 其他类的类名::成员函数名(参数表); //其他类的成员函数声明为友元

注意不要把其他类的私有成员函数声明为友元(不要抢朋友的girlfriend),下面通过例子来体会一下友元的使用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CCar;//提前声明
class CDriver{
public:
void ModifyCar(CCar* pcar);
};
//在此之前不能生成CCar对象,但是使用该类的指针和引用都是没有问题的。
class CCar{
private:
int price;
friend int MostExpensiveCar(CCar cars[],int total);
friend 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为自己的友元,这样类B的所有成员函数就都可以访问A类对象的私有成员了,定义方法如下:

1
friend class 类名

感觉还是看示例来得快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
class CCar{
private:
int price;
friend class CDriver;
};
class CDriver{
public:
CCar myCar;
void ModifyCar(){
myCar.price+=1000;//CDriver是CCar的友元,所以可以访问price
}
};

最后,注意友元的关系并不能传递,例如A是B的友元,B是C的友元,但是并不能推出A是C的友元,除非特别声明,否则这是不成立的。

this指针

this:从C到C++

C++是在C的基础上发展而来的,最初的C++编译器实际上就是将C++程序翻译成C程序,在这个过程中,会将class变成struct,对象翻译成结构变量,针对类的成员函数,在翻译时只能被翻译成全局函数,但是怎么知道这个函数是作用在哪个结构变量上的呢,这时,我们引入了this指针。例如在一个类A的成员函数Func(int p)中,在进行翻译时将会变成Func(A* this,int p),这样在执行函数时才知道你施加的对象是谁。

this指针的用处

根据上面的描述,我们知道对于非静态的成员函数实际上的形参个数比我们写入的要多一个,这个参数就是所谓的this指针,this指针指向了成员函数的作用的对象,在成员函数的执行过程中,就是通过this指针找到对象所在的地址的,因此也可以找到所有非静态成员变量的地址。例如:

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
class A{
int i;
public:
void Hello(){cout<<"Hello"<<endl;}
};
int main(){
A* p=NULL; //this指针指向NULL
p->Hello();//可以被执行,因为Hello函数中没有用到this指针的语句
}

C++中,非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象的指针,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
class Complex{
public:
double real,image;
Complex(double r,double i):real(r),image(i){}
Complex AddOne(){
this->real++;
return *this;
}
};
int main(){
Complex c1(1,1),c2(0,0);
c2=c1.AddOne();
cout<<c2.real<<','<<c2.image<<endl;
return 0;
}

第八行中this->realreal没有什么实际上的区别,this就是Complex*类型的。返回的*this实际上就是原对象经过操作后的值。最后,还是要注意,静态成员函数并不作用于某个对象,所以在其内部不能使用this指针。