【Think in C++】第11章 引用和拷贝构造函数

(一)C++中的引用

1)引用就像能够自动的被编译器间接引用的常量型指针。它通常用于函数的参数表中和函数的返回值,但也可以独立使用。

使用引用的一些规则:

     1.当引用被创建时,它必须被初始化

     2.一旦一个引用被初始化为指向一个对象,它就不能改变为另一个对象的引用(指针则可以在任何时候指向另一个对象)

     3.不可能有NULL引用。必须确保引用是一块合法的存储单元关联。

2)函数中的引用

      最经常看见引用的地方实在函数参数和返回值中。当引用被用做函数参数时,在函数内任何堆引用的更改将对函数外的参数产生改变。如果从函数返回一个引用,必须像从函数返回一个指针一样对待。当函数返回时,无论引用关联的是什么都应该存在,否则,将不知到指向哪一个内存。

#include<iostream>
#include<stdio.h>
using namespace std;
int& h()
{
    int a=10;
    return a;
}
int main()
{
    int b=h();
    cout<<b<<endl;
}

上面这段代码虽然在我的电脑上能够执行,但是会返回一个警告,因为返回了一个局部变量的引用。但是如果将函数改为这样就可以了

int& h()
{
    static int a=10;
    return a;
}
把int a定义为一个静态变量,a便有了全局的生存空间,不再只是局部变量了。


常量引用

如果一个非常量引用函数传递一个常量对象的时候,这是不合法的。但是一个常量引用函数传递一个非常量对象的时候是合法的,这时候把这个传递的非常量看成一个常量,也就代表了,如果我再这个常量引用函数里面修改了这个传递进来的非常量,那么就会出错,如果不怎么明白请看下面的程序。

//常量引用传递非常量
#include<iostream>
#include<stdio.h>
using namespace std;

void g(const int& a)
{
//    a++;   传递进来的是非常量,但是不允许修改这个非常量的值。
}
int main()
{
    int a=10;
    g(a);
    cout<<a<<endl;
}

//非常量引用传递常量
//例如下面这段程序就是错误的
#include<iostream>
#include<stdio.h>
using namespace std;

void g(int& a)
{
//    a++;   传递进来的是非常量,但是不允许修改这个非常量的值。
}
int main()
{
    const int a=10;
    g(a);
    cout<<a<<endl;
}
上面的这种做法是比较安全的。所以如果知道这个函数不妨碍对象的不变性的化,让这个参数是一个常量引用将允许这个函数在任何情况下使用。这就意味着,对于内部类型,这个函数不会改变参数,而对于用户定义的类型,该函数只能够调用常量成员函数,而且不应当改变任何的公共成员函数。

在函数参数中使用常量引用特别重要。这是因为我们的函数也许会接受临时对象,这个临时对象是由两外一个函数的返回值创立,或者由函数使用者显式创立的。临时对象总是不变的,因此如果不使用常量引用,参数将不会被编译器接受。

请看下面这个例子:

#include<iostream>
#include<stdio.h>
using namespace std;

int f()
{
    int a=1;
    return a;                //会返回一个临时对象,默认是常量
}

void g(const int& a)      //如果这边不定义为常量引用,将无法接收临时对象
{
    cout<<a<<endl;
}

int main()
{
    g(1);         //1也是一个临时对象   
 
    g(f());

}
上面这段代码,如果把函数常量引用改为函数非常量引用,便会出错了。


指针引用

在C语言中,如果想改变指针本身而不是它所指向的内容,函数可以如下声明:

void f(int**);
int i=47;
int *ip=&i;
f(&ip);
上面这段代码不得不说,是挺难看的,不怎么好理解,至少我当初学习C语言的时候就没怎么明白。
下面看C++如何来解决这个问题

//指针引用
#include<iostream>
#include<stdio.h>
using namespace std;

void increment(int*& i)
{
    i++;       //改变指针的指向,不会改变指针原来指向的内容
}

int main()
{
    int a[10]={1,2,3};
    int* i;
    i=a;
    cout<<"i= "<<i<<"     "<<*i<<endl;
    increment(i);
    cout<<"i= "<<i<<"     "<<*i<<endl;
}

//下面一段代码有错误,我也不知到哪儿错了,求高手指教
#include<iostream>
#include<stdio.h>
using namespace std;

void increment(int*& i)
{
    i++;
}

int main()
{
    int a[10]={1,2,3};
    cout<<a<<"    "<<*a<<endl;
    increment(a);
    cout<<a<<"    "<<*a<<endl;
}

擦,上面的错误找到了,谢谢小聪子了,这个错误在他辅导我的时候教训了我很多次,但是我从没有记得过,嘿嘿。数组名不能用来当作左值,希望大家注意把~~~


参数传递准则

当给函数传递参数的时候,人么习惯上是通过常量引用来传递。

这种方法可以大大提高效率:传值方式需要调用构造函数和析构函数,然而如果不想改变参数,则可以通过常量引用传递,它仅需要将地址压栈。

事实上有一种情况不适合用传递地址方式,这就是当传值是唯一安全的途径,否则将会破坏对象时。想知道原因吗?接着往下看


(二)拷贝构造函数

位拷贝与初始化

看下面一段程序

#include<iostream>
#include<stdio.h>
using namespace std;

class HowMany
{
    public:
        HowMany();
        static void print(const string& msg="");
        ~HowMany();
    private:
        static int objectCount;
};
int HowMany::objectCount=0;
HowMany::HowMany()
{
    objectCount++;
}
void HowMany::print(const string& msg)     //类里面默认实参只能在定义的时候
{
    if(msg.size()!=0)
      cout<<msg<<":";
    cout<<"objectCount = "<<objectCount<<endl;
}
HowMany::~HowMany()
{
    objectCount--;
    print("~HowMany()");
}

HowMany f(HowMany x)
{
    x.print("x argument inside f()");
    return x;
}

int main()
{
    HowMany h;
    HowMany::print("after constructor of h");
    HowMany h2=f(h);
    HowMany::print("after call to f()");
}
运行结果:

after constructor of h:objectCount = 1
x argument inside f():objectCount = 1
~HowMany():objectCount = 0
after call to f():objectCount = 0
~HowMany():objectCount = -1
~HowMany():objectCount = -2

函数f的对象是通过按值传递的方式传入的对象的拷贝。然而,参数的传递是使用C的原始的位拷贝的概念,但C++ HowMany 类需要真正的初始化来维护它的完整性。所以,默认拷贝不能达到预期的效果。调用默认的拷贝构造函数并不会调用构造函数,默认呢拷贝构造函数仅仅按位复制,所以如果一个类里面有指针的时候,那么就会造成浅复制。

拷贝构造函数

当通过按值传递的方式传递一个对象的时候,就创立了一个新的对象,函数体内的对象是由函数体外的原来存在的对象传递的。从函数体返回对象也是同样的道理。

HowMany h2=f(h);
先前未创立的对象h2是由函数f()的返回值创建的,所以又从一个现有的对象中创建了一个新对象。

编译器假定我们想使用位拷贝来创建对象。在许多情况下,这是可行的。但是在HowMany类中就行不通,因为初始化不是简单的拷贝。正如上面所说,如果类中出现一个指针又将出现另一个问题,它们指向什么内容,是否拷贝它们或者它们是否与一些新的内存快相连。

不过还好,我们可以介入这个过程。并可以防止编译器进行位拷贝。每当编译器需要才能够现有的对象创建新对象时,可以通过定义自己的函数做这些事。因为是在创建新对象,所以,这个函数应该是构造函数,并且传递给这个函数的单一参数必须是创立对象的原对象。但是这个函数不能通过按值传递的方式传入构造函数,因为我们定义的这个构造函数正是为了处理按值传递的。这里引用就起作用了,可以使用源对象的引用,这个函数被称为拷贝构造函数。

如果设计了拷贝构造函数,当从现有的对象创建新对象时,编译器将不会使用位拷贝。编译器总是调用我们的拷贝构造函数。

所以上面程序稍作修改

HowMany(const HowMany& h);   //类里面加上
HowMany::HowMany(const HowMany& h)
{
    objectCount++;
}
为什么这样修改?默认复制构造函数是进行按位拷贝,一般情况下没什么错误,但是它仅对自己的成员变量拷贝,此处objectCount是一个静态变量,是属于一个类的,并不仅仅属于一个对象,所以在进行按位拷贝的时候,并不会对他进行操作,所以我们需要定义默认构造函数来对他进行操作。

思考:如果我们函数返回没有一个对象来接收,会发生什么事情,比如f(x)?

临时对象:首先我们得先明白返回对象生成的时间。当给函数传入一个对象的时候,调用复制构造函数,函数里会有一个拷贝对象,当函数需要返回的时候,会将这个拷贝对象再次拷贝一次,把拷贝的拷贝对象返回,而紧接着原来的拷贝对象删除。那么如果我们没有一个对象来接收这个拷贝的拷贝对象怎么办?系统会生成一个临时变量来存储。但是当这句话执行完后,这个临时变量就销毁了。


默认拷贝构造函数

因为拷贝构造函数实现按值传递方式的参数传递和返回,所以在这种简单结构情况下,编译器将有效地创建一个默认拷贝构造函数。知道目前所看到的一切默认的都是原始行为“位拷贝”。

当包括更复杂的类型时,如果没有创建拷贝构造函数,C++编译器也将自动的创建拷贝构造函数。

下面有个例子。设想创建一个类,它是由某些现有类的对象组成的。这个创建类的方法被称为组合,它是从现有类创建新类的方法之一。

#include<iostream>
#include<stdio.h>
#include<string>
using namespace std;
class WithCC
{
    public:
        WithCC(){}
        WithCC(const WithCC&)          //复制构造函数
        {
            cout<<"WithCC(WithCC&)"<<endl;
        }
};

class WoCC
{
    public:
        WoCC(const string& ident=""):id(ident){}   //构造函数
        void print(const string& msg="") const
        {
            if(msg.size()!=0)
              cout<<msg<<": ";
            cout<<id<<endl;
        }
    private:
        string id;
};

class Composite
{
    public:
        Composite():wocc("Composite()"){}    //带有初始化列表的构造函数
        void print(const string& msg="")const
        {
            wocc.print(msg);
        }
    private:
        WithCC withcc;
        WoCC   wocc;
};

int main()
{
    Composite c;
    c.print("Contents of c");
    cout<<"Calling Composite copy-constructor"<<endl;
    Composite c2=c;    //Composite无复制构造函数,复制构造函数自动被创建
    c2.print("Contents of c2");
}

运行结果:

Contents of c: Composite()
Calling Composite copy-constructor
WithCC(WithCC&)
Contents of c2: Composite()
从上面这个例子来看,如果类是组合构成的,在没有定义拷贝构造函数的情况下,如果该对象原先的类有拷贝构造函数,那么就调用它自身的拷贝构造函数,如果没有则进行位复制。


替代拷贝构造函数的方法

有时候为了安全,我们直接不用拷贝构造函数,以防止出现一些错误

有一个简单的技术防止通过按值传递方式传递:声明一个私有拷贝构造函数。甚至不必去定义它,除非成员函数或友元函数需要执行按值传递方式的传递。如果用户试图用按值传递或返回对象,编译器将会发出一个出错信息。这是因为拷贝函数是私有的。因为已显示地声明我们接管了这项工作,所以编译器不再创建默认的拷贝构造函数。

#include<iostream>
#include<stdio.h>
#include<string>
using namespace std;

class NoCC
{
    public:
        Nocc(int ii):i(ii){}
    private:
        int i;
        NoCC(const NoCC&);
};
void f(NoCC);
int main()
{
    NoCC n;
    f(n);    //Error
    NoCC n2=n;  //Error
    NoCC n3(n); //Error
}
上面这样就不能使用复制构造函数了。


(三)指向成员的指针

指针是指向一些内存的地址的变量,既可以是数据的地址也可以是函数的地址。所以,可以在运时改变指针指向的内容。C++的成员指针遵从同样的概念,除了所选择的内容是在类中之内的成员指针。这里麻烦的是所有指针需要地址,但是在类内部是没有地址的;选择一个类的成员意味着在类中偏移。只有把偏移和具体对象的开始地址相结合,才能得到实际地址。成员指针的语法要求选择一个对象的同时间接引用成员指针。

看了好一会儿,不理解,这掌的最后一部分先放这儿,慢慢理解。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值