参考:http://www.weixueyuan.net/view/6344.html

总结:

  如果拷贝构造函数的参数不是对象的引用,则是不允许的。如 book(book b); 是无法编译通过的。

  拷贝构造函数除了能有对象引用这样的参数之外,同样也能有其它参数。但是其它参数必须给出默认值。

  默认拷贝构造函数的问题, 拷贝构造函数参数为引用,系统自动生成的拷贝构造函数功能简单,只是将arr1的数组首地址直接赋值给arr2的数组首地址。

  如果类的设计人员不在类中显示的声明一个拷贝构造函数,则系统会自动地为类生成一个拷贝构造函数,自动生成的拷贝构造函数功能简单。

  由于类会自动生成拷贝构造函数,因此有些时候为了不让对象发生拷贝行为,我们可以显示声明一个拷贝构造函数,并将其设置为private属性。

拷贝构造函数,顾名思义,就是通过拷贝对象的方式创建一个新对象。拷贝构造函数有两种原型(我们继续以book类来说明拷贝构造函数原型):

  1. book(book &b);
  2. book(const book &b);

这两种原型都是book类对象的引用。下面一种原型则规定在创建新对象的时候不得修改被拷贝的对象。如果拷贝构造函数的参数不是对象的引用,则是不允许的。如下面这种构造函数形式则是无法编译通过的。

  1. book(book b);

为什么拷贝构造函数的参数一定要是对象的引用呢?我们可以想一下,如果不是引用,而是通过传值的方式将实参传递给形参,这中间本身就要经历一次对象的拷贝的过程,而对象拷贝则必须调用拷贝构造函数,如此一来则会形成一个死循环,无解。所以拷贝构造函数的参数必须是对象的引用。

拷贝构造函数除了能有对象引用这样的参数之外,同样也能有其它参数。但是其它参数必须给出默认值。例如下面这种拷贝构造函数声明方式。

  1. book(const book &b, price = 5.0);

如果类的设计人员不在类中显示的声明一个拷贝构造函数,则系统会自动地为类生成一个拷贝构造函数,自动生成的拷贝构造函数功能简单,只能将源对象的所有成员变量一一复制给当前创建的对象。

例1:

class book
{
public:
book(){}
book(book &b);
book(char* a, double p = 5.0);
void display();
private:
double price;
char * title;
}; book::book(book &b)
{
price = b.price;
title = b.title;
} book::book(char* a, double p)
{
title = a;
price = p;
} void book::display()
{
cout<<"The price of "<<title<<" is $"<<price<<endl;
}

在本例中的book类中就声明了一个拷贝构造函数book(book &b);当然这个拷贝构造函数跟系统默认生成的拷贝构造函数功能是一样的,也就只是实现了数据成员的对应拷贝功能。

了解了拷贝构造函数的声明及定义方式,我们再来看一下我们在设计类的时候,什么时候才需要设计拷贝构造函数,我们先来看下面一个例子,相信看完之后会有一定领会,之后再来揭晓答案。

例2:

#include<iostream>
using namespace std; class Array
{
public:
Array(){length = ; num = NULL;};
Array(int * A, int n);
void setnum(int value, int index);
int * getaddress();
int getlength(){return length;}
void display();
private:
int length;
int * num;
}; Array::Array(int *A, int n)
{
num = new int[n];
length = n;
for(int i=; i<n; i++)
num宁波优学智能科技有限公司[i] = A[i];
} void Array::setnum(int value, int index)
{
if(index < length)
num[index] = value;
else
cout<<"index out of range!"<<endl;
} void Array::display()
{
for(int i=; i<length; i++)
cout<<num[i]<<" ";
cout<<endl;
} int * Array::getaddress()
{
return num;
} int main()
{
int A[] = {,,,,};
Array arr1(A, );
arr1.display();
Array arr2(arr1);
arr2.display();
arr2.setnum(,);
arr2.display();
arr1.display();
cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
return ;
}

程序运行结果如下:

1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 8 4 5
00331F58 00331F58

在本例中,我们重新定义了一个Array类,可以理解为一个整形数组类,这个类中我们定义了两个成员变量:整形指针num和数组长度length。

类中定义了一个默认构造函数,声明了一个带参构造函数。默认构造函数很简单,带参构造函数则是用于将一个已有的数组全部拷贝给类对象。

除了两个构造函数之外,我们还定义四个成员函数,一个是用于修改数组中数值的setnum函数、一个打印数组中所有元素的display函数、一个返回数组首地址的函数getaddress和一个返回数组长度的函数getlength。除了默认构造函数之外和getlength函数之外,所有的函数在类外都有定义。

接下来我们看一下主函数。主函数中,我们先定义了一个数组,包含五个元素,分别是从1到5。之后用Array类创建对象arr1,并且用A数组初始化对象arr1,此时arr1对象相当于拥有一个数组,该数组包含5个元素,打印出来的结果是“1 2 3 4 5 ”,没有问题。之后用arr1对象初始化arr2对象,因为我们在类中没有显示地定义一个拷贝构造函数,因此系统会自动为我们生成一个拷贝构造函数,该拷贝构造函数的定义如下:

  1. Array::Array(Array &a)
  2. {
  3. length = a.length;
  4. num = a.num;
  5. }

通过系统自动生成的拷贝构造函数完成arr2对象的创建,同样的arr2也是有5个元素的数组,打印出来的结果是“1 2 3 4 5 ”,同样没有问题。

之后我们调用成员函数setnum,将arr2对象下标为2的元素修改为8(原先是3)。此时打印arr2中数组元素,结果为“1 2 8 4 5 ”,正确,arr2第三个元素确实被修改掉了。

之后我们再调用arr1.display(),奇怪的事情发生了,它的打印结果竟然也是“1 2 8 4 5 ”!我们之前并未修改过第三个元素的值的,这是怎么一回事呢?不急,我们再来看一下最后一句“cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;”其显示结果竟然是一样的!看到这里是不是有些明白了上面的问题呢?很明显,arr1和arr2所指向的数组是同一个数组,在内存中的位置是一致的,因此当我们利用对象arr2去修改数组中第三个元素的数值的时候,arr1中的数组也被修改了,其实它们本来就是使用的是同一个内存中的数组而已。

这问题是怎么产生的呢?不难想到拷贝构造函数参数为引用,系统自动生成的拷贝构造函数功能简单,只是将arr1的数组首地址直接赋值给arr2的数组首地址,也即num = a.num;这必然导致两个对象指向同一块内存。既然问题出在系统自动生成的拷贝构造函数上,自然要从拷贝构造函数上下手了。下面我们将正确的程序展示如例3。

例3:

#include<iostream>
using namespace std; class Array
{
public:
Array(){length = ; num = NULL;};
Array(int * A, int n);
Array(Array &a);
void setnum(int value, int index);
int * getaddress();
void display();
int getlength(){return length;}
private:
int length;
int * num;
}; Array::Array(Array & a)
{
if(a.num != NULL)
{
length = a.length;
num = new int[length];
for(int i=; i<length; i++)
num[i] = a.num[i];
}
else
{
length = ;
num = ;
}
} Array::Array(int *A, int n)
{
num = new int[n];
length = n;
for(int i=; i<n; i++)
num[i] = A[i];
} void Array::setnum(int value, int index)
{
if(index < length)
num[index] = value;
else
cout<<"index out of range!"<<endl;
} void Array::display()
{
for(int i=; i<length; i++)
cout<<num[i]<<" ";
cout<<endl;
} int * Array::getaddress()
{
return num;
} int main()
{
int A[] = {,,,,};
Array arr1(A, );
arr1.display();
Array arr2(arr1);
arr2.display();
arr2.setnum(,);
arr2.display();
arr1.display();
cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl;
return ;
}
  1. 程序运行结果如下所示:

1 2 3 4 5
1 2 3 4 5
1 2 8 4 5
1 2 3 4 5
00311F58 00487268

看例3运行结果,如此一来,程序运行结果正确,而且两个对象arr1和arr2所指向的内存空间也是不一样的。我们在例3中自己定义了一个拷贝构造函数,并且开辟了一个新的空间用于存储数据。如此一来当然是不会有问题的了。本例中所介绍的是一个非常微妙的错误,在程序设计过程中,一定要加以避免。

从这个例子中我们是不是领会到了什么呢?通常,如果一个类中包含指向动态分配存储空间的指针类型的成员变量时,就应该为这个类设计一个拷贝构造函数,除了需要设计一个拷贝构造函数之外,还需要为它添加一个赋值操作符重载函数(即重载“=”操作符,这将会在操作符重载那一章加以介绍)。

由于类会自动生成拷贝构造函数,因此有些时候为了不让对象发生拷贝行为,我们可以显示声明一个拷贝构造函数,并将其设置为private属性。这跟通过将默认构造函数设置成private属性限制对象的创建时一样的道理。当然,禁止对象发生拷贝的需求较少,如果有这样的需求的话,知道还可以这么做就足够了,这是一个类设计的技巧。

2.13 C++拷贝构造函数的更多相关文章

  1. 深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结

    转自 http://www.jb51.net/article/37527.htm,感谢作者 #include "stdafx.h"      #include <iostre ...

  2. [Reprint]C++友元函数与拷贝构造函数详解

    这篇文章主要介绍了C++友元函数与拷贝构造函数,需要的朋友可以参考下   一.友元函数 1.友元函数概述: (1)友元函数是定义在一个类外的普通函数.友元函数和普通函数的定义一样;在类内必须将该普通函 ...

  3. C++的转换构造函数、拷贝构造函数、赋值运算符重载

    1 转换构造函数     C++的转换构造函数是只有一个参数的构造函数.当程序试图将一个其他类型的对象或基本类型值赋给该类的一个待初始化对象时(如Person p="Dean";) ...

  4. 拷贝构造函数,深拷贝,大约delete和default相关业务,explicit,给定初始类,构造函数和析构函数,成员函数和内联函数,关于记忆储存,默认参数,静态功能和正常功能,const功能,朋友

     1.拷贝构造 //拷贝构造的规则,有两种方式实现初始化. //1.一个是通过在后面:a(x),b(y)的方式实现初始化. //2.另外一种初始化的方式是直接在构造方法里面实现初始化. 案比例如以 ...

  5. 从零开始学C++之构造函数与析构函数(二):初始化列表(const和引用成员)、拷贝构造函数

    一.构造函数初始化列表 推荐在构造函数初始化列表中进行初始化 构造函数的执行分为两个阶段 初始化段 普通计算段 (一).对象成员及其初始化  C++ Code  1 2 3 4 5 6 7 8 9 1 ...

  6. C++知识点:拷贝构造函数例子

    //拷贝构造函数: //函数参数传递时调用一次拷贝构造函数,给对象赋值时调用一次拷贝构造函数,对象作为参数传递后会被及时销毁. #include <fstream> #include &l ...

  7. 【C++】拷贝构造函数和赋值符函数

    在C++中,调用拷贝构造函数有三种情况: 1.一个对象作为函数参数,以值传递的方式传入函数体. 2.一个对象作为函数返回值,以值传递的方式从函数返回. 3.一个对象用于给另外一个对象进行初始化(复制初 ...

  8. [C++ Primer] : 第13章: 拷贝控制

    拷贝, 赋值与销毁 当定义一个类时, 我们显示地或隐式地指定在此类型的对象拷贝, 移动, 赋值和销毁时做什么. 一个类通过定义5种特殊的成员函数来控制这些操作, 包括: 拷贝构造函数, 拷贝赋值运算符 ...

  9. 初始化列表(const和引用成员)、拷贝构造函数

    一.构造函数初始化列表 推荐在构造函数初始化列表中进行初始化 构造函数的执行分为两个阶段 初始化段 普通计算段 (一).对象成员及其初始化  C++ Code  1 2 3 4 5 6 7 8 9 1 ...

随机推荐

  1. 中文情况下,Eclipse的最好字体。

    个人喜欢的是 Microsoft YaHei Mono 了. 下面的文章喜欢的是 YaHei Consolas Hybrid. 字体安装方法的话,拷贝到 widnows\fonts目录就行. http ...

  2. xml ----> 几个常用dtd头文件模板

    环境: idea ce 2018.1 "File --> settings... --> Editor --> file and code templates" ...

  3. 三个解释——MVC的网址

    [三个MVC网址]1.https://www.cnblogs.com/sunniest/p/4555801.html2.https://www.cnblogs.com/wmyskxz/p/884846 ...

  4. linux bash基本特性

    一.bash 基础特性 (1)命令历史的功能 history: 环境变量 HISTSIZE:命令历史记录的条数 HISTFILE: ~/.bash_history 每个用户都有自己独立的命令历史文件 ...

  5. MVC实战之排球计分(二)—— 构架概要设计

    本程序主要基于MVC4框架,使应用程序的输入,处理和输出强制性分开,使得软件可维护性,可扩展性,灵活性以及封装性得到提高, MVC应用程序分为三个核心部件:Model,View, Controller ...

  6. 【Java】【6】JDK8 Stream操作整理

    摘要: 1,List<EntityOld>转换为List<EntityNew> List<EntityOld> list = oldList; List<En ...

  7. Android之Activity生命周期详解

    Activity的生命周期方法: onCreate()--->onStart()--->onResume()--->onPause()--->onStop()--->on ...

  8. python目录和文件操作

    一.python中对文件.文件夹操作时经常用到的os模块和shutil模块常用方法. 要操作目录,必须引入os模块 import os1.得到当前工作目录,即当前Python脚本工作的目录路径: os ...

  9. 关于cf[转]

    还不怎么熟悉cf呢.. 你应当知道的关于Codeforces的事情 Codeforces简称: cf(所以谈论cf的时候经常被误会成TX的那款游戏).网址: codeforces.com 这是一个俄国 ...

  10. [codechef July Challenge 2017] IPC Trainers

    IPCTRAIN: 训练营教练题目描述本次印度编程训练营(Indian Programming Camp,IPC)共请到了 N 名教练.训练营的日程安排有 M 天,每天最多上一节课.第 i 名教练在第 ...