程序设计与算法三~C++面向对象程序设计~北大郭炜MOOC学习笔记~第三章:类和对象进阶(新标准C++程序设计)

本文详细讲解了C++中的构造函数、复制构造函数、析构函数和友元概念,包括它们的作用、用法示例和特殊场景。理解这些概念有助于提升对类和对象管理的理解。

以下内容为笔者手打,望读者珍惜,如有转载还请注明。

chapter 3:类和对象进阶
$3.1构造函数
$3.1.1 构造函数的概念和作用
    全局变量在程序装入内存时就已经分配好了存储空间,程序运行期间其地址不变.对于程序员
没有初始化的全局变量,程序启动时自动将其全部初始化为0(即变量的每个比特都是0).在大多数
情况下,这是一种稳妥的做法,并且将全局变量全自动部初始化为0是程序启动时的一次性操作,不
耗时间.
    对于局部变量,如果不进行初始化,那么他的初始值是随机的,局部变量定义在函数内部,其
存储空间是动态分配在栈中的.函数被调用时,栈会分配一部分空间存放该函数中的局部变量(包括
参数),这片新分配出来的存储空间的中原来的内容是什么,局部变量的初始内容就是什么,因此
局部变量的初始值是不可预测的.函数调用结束后,局部变量占用的存储空间就被回收,以便
分配给下一次函数调用中涉及的局部变量.
    那么为什么不将局部变量自动初始化为0呢?因为一个函数的局部变量在内存中的地址,在函数
每次被调用时都有可能不同,因此自动初始化工作不是一次性的,而是函数每次调用的时候都要做,
这就带来无谓的时间开销.

    构造函数是一类特殊的成员函数,其名字和类的名字一样,不写返回值类型(void也不写),
可以重载,即一个类可以有多个构造函数.构造函数执行时,对象的内存空间已经分配好了,构造
函数的作用是初始化这片空间.如果编写了构造函数,编译器就不会生成无参的默认构造函数.

例: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;//无法通过编译(Complex类没有无参构造函数)
Complex *pc = new Complex;//错误,Complex类没有默认构造函数


举例:class Test {
public:
Test( int n) { } //(1)
Test( int n, int m) { } //(2) 
Test() { } //(3)
};
Test array1[3] = { 1, Test(1,2) };
// 三个元素分别用(1),(2),(3)初始化
Test array2[3] = { Test(2,3), Test(1,2) , 1};
// 三个元素分别用(2),(2),(1)初始化
Test * pArray[3] = { new Test(4), new Test(1,2) };
//两个元素分别用(1),(2) 初始化
pArray数组是指针数组,其元素不是CTest 类的对象,而是CTest 类的指针.上面对pArray[0]
和pArray[1]进行了初始化,把他们初始化为指向动态内存分配的CTest对象的指针.而这两个
动态分配出来的CTest对象又分别是用(1),(2)两个函数初始化的.pArray[2]没有初始化,其值
是随机的,不知道指向哪里.所以pArray那行只生成了两个CTest对象,而不是三个,因此只调用了
两次CTest类构造函数.

$3.1.3复制构造函数
    复制构造函数时构造函数的一种,又称拷贝构造函数,它只有一个参数,参数类型是本类的
引用.复制构造函数的参数可以是const引用,也可以是非const引用,一般选择前者.
    如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数.大多数情况下,其作用
是实现从源对象到目标对象逐个字节的复制,即使得目标对象的某一个成员变量都与源对象相等.
    如果编写了复制构造函数,则默认复制构造函数就不存在了.

    自己编写的复制构造函数不一定要做复制的工作(如果只做复制的工作,使用编译器自动生成的
默认复制构造函数就行了).但是从习惯上来讲,复制构造函数还是完成类似于复制的工作为好,
在此基础上还可以根据需要做一些其他的操作.

    Complex(Complex & c);
    Complex(const Complex & c);

复制构造函数被调用的三种情况:
(1)当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用.
Complex c2(c1); Complex c2=c1;//这两条语句都会引发复制构造函数的调用.

(2)如果函数F的参数是类A的对象,那么当F 被调用时,类A的复制构造函数将被调用.
由于复制构造函数可以自定义,我们知道,函数的形参不一定等于函数调用时对应的实参.
如果形参是一个对象,那么形参的值是否等于实参取决于该对象所属的类的复制构造函数
是如何实现的.

如果我们用const引用就不需要调用复制构造函数传参也不用担心对象的值被修改
例:void CFunc(const Complex &c)

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

$3.1.4类型转化构造函数
除复制构造函数外,只有一个参数的构造函数一般都可以称作类型转换构造函数,因为这样的函数
可以起到类型自动转化的作用.
class Complex{
    Complex(int i){//类型转化构造函数

    }
    Complex(double r,double i){

    }
};
int main()
{
    Complex c1(7,8);
    Complex c2=12;//本行是初始化语句而不是赋值语句编译器直接以12作为参数调用Complex
(int)构造函数来初始化c2.
    c1 = 9;//这条赋值语句等号两边的类型是不匹配的,之所以不会报错,是因为Complex(int)
这个类型转化构造函数能够接受一个整型参数.因此,编译器在处理这条语句时,会在等号右边
临时的生成一个Complex对象,该临时对象以9为实参,用Complex(int)构造函数初始化,然后将
这个临时对象的值赋给c1,也可以说9被自动转成一个Complex对象后赋值给c1.
}

$3.2析构函数
    析构函数(destructor)是成员函数的一种,它的名字与类名相同,前面加“~”,没有参数和
返回值.一个类有且仅有一个析构函数.如果定义类时没写析构函数,编译器生成默认的析构函数.
如果定义了析构函数,则编译器不生成默认的析构函数.
    析构函数在对象消亡的时候自动被调用.可以定义析构函数在对象消亡前做善后工作.例如,
对象如果在生存期间用new运算符动态分配了内存,则在各处写delete匀速那副确保程序的每条
执行路径都能释放这条内存是比较麻烦的事情.有了析构函数,只要在析构函数中调用delete语句
,就能确保对象运行期间new运算符分配的空间在对象消亡时被释放.
例:class String{
    private:
        char *p;
    public:
        String (int n);
        ~String();
};
String::~String(){
    delete []p;
}
String::String(int n){
    p=new char[n];
}

$3.4静态成员变量和静态成员函数
    类的静态成员有两种:静态成员变量和静态成员函数.静态成员变量就是在定义时前面加了
static 关键字的成员变量;静态成员函数就是在声明前面加了static关键字的成员函数.
    普通成员变量每个对象有各自的一份,而静态成员变量只有一份,被所有同类对象共享.
普通成员函数一定是具体作用在某一个对象上的,而静态成员函数并不具体的作用在某一个对象上.
访问普通成员时,通过“对象名.成员名”;访问静态成员时,则可以通过“类名::成员名”方式访问,
不需要指明被访问的对象属于哪一个对象或作用于哪一个对象.因此,可以在还没有任何对象生成
的时候就先访问一个类的静态成员.
    使用sizeof运算符计算对象所占用的内存空间时,不会将静态成员变量计算在内.
    静态成员变量本质上是全局变量.一个类,哪怕一个对象都不存在,其静态成员变量也存在.
静态成员函数并不需要作用在某一个具体的对象上,因此其本质上也是全局函数.
    设置静态成员的目的,是为了将和某些类紧密相关的全局变量和全局函数写到类里面,形式
上成为一个整体.
Class CRectangle{
    private:
        static int totalArea;
        static int totalNumber;
}
int CRectangle::totalArea=0;
int CRectangle::totalNumber=0;
必须在定义类的文件中对静态成员变量进行一次声明或者初始化,否则编译能通过,但是链接
(link)阶段会报告“标识符找不到”,不能生成.exe文件.

//注:在main函数里面还是不能“cout<<CRectangle::totalNumber;”(私有化)

因为静态成员函数不具体作用于某一个对象,所以静态成员函数内部不能访问非静态成员变量,
也不能调用非静态成员函数.//因为不知道调用/访问的属于哪一个对象

$3.5常量对象与常量成员函数
    如果希望某个对象的值初始化后就不再改变,则定义该对象的时候可以在前面加const关键字,
使之成为常量对象.
例如:class CDemo{
    public:
        void SetValue(){ }
};
const CDemo Obj; // Obj是常量对象
在Obj被定义为常量对象时,下面这条语句时错误的,编译不能通过:Obj.SetValue();
    错误的原因是:常量对象一旦初始化后,其值就再也不能修改.因此不能通过常量对象调用
普通成员函数,因为调用过程中常量对象可能被修改.
    但是可以通过常量对象调用常量成员函数.所谓常量成员函数,就是在定义时加了const关键字
的成员函数,声明时也要加!
class Sample{
    public:
        void GetValue() const;//常量成员函数定义时要加const关键字
};
void Sample::GetValue()const//常量成员函数声明的时候也要加const关键字
{
}
int main()
{
    const Sample o;
    o.GetValue();//常量对象上可以执行常量成员函数
}
常量成员函数内部不允许调用同类的其他非常量成员函数(静态成员函数除外).

两个成员函数的名字和参数表相同,但一个是const,一个不是,则它们算重载.

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

$3.6.1封闭类构造函数的初始化列表
类名::构造函数的名(参数表):成员变量1(参数表),成员变量2(参数表)……
{
    ……
}
其中“:”和“{”之间的就是初始化列表.初始化列表中的成员变量既可以是成员对象,也可以是
基本类型的成员变量.对于成员对象,初始化列表的“参数表”中存放的是构造函数的参数(它指
明了该成员对象如何初始化).对于基本类型成员变量,“参数表”就是一个初始值.“参数表”中
的参数可以是任何有定义的表达式,可以包括变量甚至函数调用……

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;
}

封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数.
成员对象构造函数的执行次序和成员对象在类中定义的次序一致,与它们在构造函数初始化
列表中的顺序无关.当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的
析构函数.(封闭类的析构函数中可能会用到成员对象)

封闭类的对象初始化时,要先执行成员对象的构造函数,是因为封闭类的构造函数中有可能
用到成员对象.如果此时成员对象还没有初始化,就不太合理.

$3.6.2 封闭类的复制构造函数
封闭类的对象,如果是用默认复制构造函数初始化的,那么它包含的各个对象也会用复制构造
函数初始化.

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.

$3.7 const成员和引用成员
类还可以有常量型成员变量和引用型成员变量.这两种类型的成员变量必须在构造函数的初始化
列表中进行初始化.常量型成员变量的值一旦初始化,就不能再改变.
例:
int f;
class CDemo{
    private:
        const int num;//常量型成员变量
        int & ref;//引用型成员变量
        int value;
    public:
        CDemo(int n):num(n),ref(f),value(4)
        {
        }
};//sizeof(CDemo)=12.

$3.8 友元
朋友是值得信任的,所以可以对它们公开一些自己的隐私.
友元分为两种:友元函数和友元类.
$3.8.1 友元函数
在定义一个类时,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样这些
函数就成为了该类的友元函数,在友元函数内部就可以访问该类的私有成员了.
将全局函数声明为友元的写法如下:
friend 返回值类型 函数名(参数表);
将其他类的成员函数声明为友元写法如下:
friend 返回值类型 其他类的类名::成员函数名(参数表);

但是,不能把其他类的私有成员函数声明为友元.
举例:
#include<iostream>
using namespaces std;
class CCar;//提前声明CCar类,以便后面的CDriver类使用
class CDriver{
    public:
        void ModifyCar(CCar* pCar);
};
class CCar{
    private:
        int price;
    friend int MostExpensiveCar(CCar cars[],int total);//全局函数MostExpensiveCar是CCar类的友元函数
    friend void CDriver::ModifyCar(CCar* pCar);//CDriver类的成员函数ModifyCar是CCar类的友元函数
};
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;
}
int main()
{
    return 0;
}

上面的程序中我们提前声明了CCar类,CCar类的定义在后面.之所以要提前声明,是因为CDriver
类的定义中用到了CCar类型,若此时CCar类还没有定义会编译报错.需要注意到,直接把CCar类的
定义写在CDriver类前面是解决不了问题的,因为CCar类的定义中需要声明CDriver类的友元函数.
因此C++允许将一个类的名字提前声明:
class 类名;
尽管可以提前声明,但在一个类的定义出现前,仍然不能有任何会导致该类的对象生成的语句,
不过使用该类的引用或者指针是可以的.

$3.8.2友元类
一个类A可以将另一个类B的所有成员声明为自己的友元,类B的所有成员函数也就可以访问类A对象
的所有私有成员.在类定义中声明友元类的写法如下:
firend class 类名;
举例:class CCar{
    private:
        int price;
        friend class CDriver;//声明CDriver是CCar的友元类,这条语句本身就在声明CDriver是
        //一个类,所以在CCar的定义前面就不必在声明CDriver类了.
};
class CDriver{
    public:
        CCar myCar;
        void ModifyCar{
            myCar.price+=1000;
        }
};
int main()
{
    return 0;
}

$3.9 this指针
$3.9.1C++程序到C程序的翻译
举例:C++程序
class CCar{
    public:
        int price;
        void SetPrice(int p);
};
void CCar::SetPrice(int p){
    price=p;
}
int main()
{
    CCar car;
    car.SetPrice(20000);
    return 0;
}

翻译后的C程序:
struct CCar{
    int price;
};
void SetPrice(CCar* price,int p){
    this->price=p;
}
int main()
{
    struct CCar car;
    SetPrice(&car,20000);
    return 0;
}

类被翻译成结构体,对象翻译成结构变量,成员函数翻译为全局函数.不过C程序的全局函数
SetPrice比C++的成员函数SetPrice多了一个参数,就是“CCar*this”.“Car.SetPrice(20000);”
被翻译成“SetPrice(&car,20000);”后者在执行时,this形参指向的正是car这个变量,因而
达到了SetPrice 函数作用在car 变量的效果.

$3.9.2 this指针的作用
非静态成员函数实际上的形参个数比程序员写的多一个,多出来的就是所谓的“this指针”.这个
“this指针”指向了成员函数作用的对象,在成员函数作用过程中,正是通过“this指针”才能找到
对象所在的地址,因而找到对象的所有非静态成员变量的地址.
例:#include<iostream>
using namespace std;
class A{
    int i;
    public:
        void Hello(){cout<<"hello"<<endl;}
};
int main()
{
    A* p=NULL;
    p->Jello();
}
//程序的输出结果为hello
在上面的程序中,p是一个空指针,"p->Hello"实际上应该是"Hello(p)",在翻译后的Hello函数
中,cout语句没有用到this指针,因此依然可以输出结果.如果Hello函数中有对成员变量的访问
程序就会出错.

C++规定,在非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象
的指针.
#include<iostream>
using namespace std;
class Complex{
    public:
        double real,imag;
        Complex(double r,double i):real(r),imag(i){}
        Complex AddOne(){
            this->real++;//this指针的类型是Complex*.因为this指针就指向函数所指的对象,
            //所以“this->real”与“对象.real”是完全等价的.
            return *this;
        }
};
int main()
{
    Complex c1(1,1),c2(0,0);
    c2=c1.AddOne();//进入AddOne函数后,"*this"实际就是c1,因此c2的值会变得与c1相同.
    cout<<c2.real<<","<<c2.imag<<endl;//输出2,1
    return 0;
}

静态成员函数并不作用于某一个对象,所以在其内部不能使用this指针,否则这个指着不知道指向
哪一对象.

$3.10 在多个文件中使用类

以上内容为笔者手打,望读者珍惜,如有转载还请注明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值