GeekBand学习笔记 - 关于C++中的拷贝构造,赋值,析构

写在前面:感谢GeekBand提供这样好的学习机会,让我在繁忙的工作之余可以学习巩固c++知识。以下是边学边记的一些扩展点。分享给大家。

关于构造函数,特别要说到显式构造函数
http://zhan.renren.com/dpsincs?gid=3602888497997553533
定义: 通常, 如果构造函数只有一个参数, 可看成是一种隐式转换. 打个比方, 如果你定义了Foo::Foo(string name), 接着把一个字符串传给一个以 Foo 对象为参数的函数, 构造函数Foo::Foo(string name) 将被调用, 并将该字符串转换为一个 Foo 的临时对象传给调用函数. 看上去很方便, 但如果你并不希望如此通过转换生成一个新对象的话, 麻烦也随之而来. 为避免构造函数被调用造成隐式转换, 可以将其声明为 explicit.

优点: 避免不合时宜的变换.
缺点:无
结论: 所有单参数构造函数都必须是显式的. 在类定义中, 将关键字 explicit 加到单参数构造函数前:explicit Foo(string name);
例外: 在极少数情况下, 拷贝构造函数可以不声明成 explicit. 作为其它类的透明包装器的类也是特例之一. 类似的例外情况应在注释中明确说明.

struct VS class
仅当只有数据时使用 struct, 其它一概使用 class.
在 C++ 中 struct 和 class 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字.
struct 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域 (field), 而非函数调用. 除了构造函数, 析构函数,Initialize(), Reset(), Validate() 外, 不能提供其它功能的函数.
如果需要更多的函数功能, class 更适合. 如果拿不准, 就用 class.
为了和 STL 保持一致, 对于仿函数 (functors) 和特性 (traits) 可以不用 class 而是使用 struct.
注意: 类和结构体的成员变量使用 不同的命名规则.

关于引用(来自http://www.weixueyuan.net/view/5840.html

◆ 1 什么是引用?

引用又称别名(alias),是一种非常特殊的数据类型。它不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是C++系统不为引用类型变量分配内存空间。引用主要用于函数之间的数据传递。

引用定义的格式为:类型 &引用变量名=已定义过的变量名;

例如

double number ;
double &newnum=number ;

newnum是新定义的引用类型变量,它是变量number的别名,两个变量代表同一块内存。

◆ 2 使用一个函数交换两个数据

#include<iostream.h>
void swap(double & d1,double & d2) //d1和d2是引用型变量
{
       double temp ;
       temp=d1 ;
       d1=d2 ;
       d2=temp ;
}

void main(void)
{
       double x , y ;
       cout<<"请输入x和y的值"<<'\n';
       cin>>x>>y ;
       swap(x,y) ;
       cout<<"x="<<x<<'\t'<<"y="<<y<<'\n';
}

◆ 3 引用可以作为函数的返回值
一般函数返回值时,要生成一个临时变量作为返回值的拷贝,而用引用作为返回值时,不生成值的拷贝。

采用不同返回方式的求正方形面积函数的比较。

#include<iostream>
using namespace std;
double temp;
double fsqr1(double a){
    temp=a*a ; return temp;
}
double & fsqr2(double a){
    temp=a*a ; return temp;
}
int main(){
    double x=fsqr1(5.5);//第一种情况
    double y=fsqr2(5.5);//第二种情况
    cout<<"x="<<x<<'\t'<<"y="<<y<<endl;
    return 0;
}

几点注意:
对数组只能引用数组元素,不能引用数组(数组名本身为地址)。
不能定义引用的引用(引用也是地址),所以当函数的参数为引用时,引用不能作实参。
C++回顾:声明、定义、初始化与赋值
(来自https://chuan92.com/2015/10/26/declaration-definition-initialization-cpp
声明的作用是指定变量的类型和名称,makes a name known to the program。区分声明和定义可以让C++支持分开编译,比如A.cpp中定义了变量var1,在B.cpp中只需要声明var1这个变量就可以直接使用。因为这样的用法,声明常常见于头文件中。源文件包含头文件之后,就可以使用这个变量,即使没有看到该变量的定义。 声明的语法如下:

extern int i; // declaration
int j; // definition
extern double pi = 3.1416; // definition

定义是为变量分配存储空间,并可能进行初始化。定义是一种声明,因为定义的同时必然会指定变量的类型和名称,然而声明却不是定义。C++中变量的定义必须有且仅有一次,而变量的声明可以多次。变量一般不能定义在头文件中,除了const变量(local to a file)。

除了变量,类和函数也有定义的说法,总结如下:

对于类来说,一般定义在头文件中。因为编译器需要在每个源文件都看到类的定义才能编译成功;
对于一般函数来说,函数声明在头文件中,函数定义则在源文件中;
对于inline和constexpr function,编译器需要在每个源文件都看到定义,因此通常也定义在头文件中;

初始化是指变量在创建的同时获得的初始值。虽然C++经常用=来初始化一个变量,但是赋值和初始化是两种不同的操作。赋值是变量定义后的操作,效果是改变变量的值,或者说是用新值来替换旧值;而初始化是在变量创建期获得一个值。两者具有本质的区别。下面分别介绍一下C++常见的初始化方式:
default initialization

当我们定义一个变量时,不提供initializer,那么这个变量就是默认初始化(default initialized)的。默认值由变量的类型和变量的定义位置来决定。

对于built-in type,默认值由变量的定义位置决定。在函数外部定义的全局变量(global variable),函数内部定义的局部静态变量(local static object)全部初始化为0。函数内部定义的局部变量,以及类中不在初始化成员列表和构造函数里体的成员变量都是未初始化的;使用未初始化的变量值的行为是未定义的,编译器不保证不会自燃。
对于class type,由类里的默认构造函数初始化。如果类定义里没有默认构造函数(显示或隐示),则编译出错。

list initialization

C++11中提供了一种新的初始化方式,list initialization,以大括号包围。A tour of c++中写到The = form is traditional and dates back to C, but if in doubt, use the general {}-list form。注意这种初始化方式要求提供的初始值与要初始化的变量类型严格统一,用法如下,

//built-in type initialization
double d1{2.3};              //ok: direct-list-initialization 
double d2 = {2.3};           //ok: copy-list-initialization
//class type initialization
complex<double> z2 {d1,d2};
complex<double> z3 = {1,2};  //ok: the = is optional with {...}
vector<int> v {1,2,3,4,5,6}; //ok: a vector of ints

long double pi = 3.1415;
int a{pi}, b = {pi};         //error: narrowing conversion required.
int c(pi), d = pi;           //ok: implict conversion.

value initialization

value initialization里,built-in type变量被初始化为0,class type的对象被默认构造(一定要有)初始化。这种方式通常见于STL里的vector和数组,且经常与list initialization结合起来使用,为我们初始化全0数组提供了很大的便利。简单用法如下:

vector<int> ivec(10);           //ten elements, each initialized to 0
vector<string> svec(10);        //ten elmenets, each an empty string
vector<string> v1 = {"a", "an", "the"};     //list initialized
int a[10] = {};                 //ten elements, each initialized to 0
int a2[] = {1,2,3};             //list initialized
int a3[5] = {1,2,3};            //equivalent to a3[] = {1,2,3,0,0}

关于类的初始化比较复杂,整理几点:
编译器首先编译类成员的声明,包括函数和变量
整个类可见后,才编译函数体(所以不管定义顺序,函数里可以用类里的任何变量和函数)
C++11提供了in-class initializers机制。注意这种机制只支持=,{}形式,不支持()。Constructor Initializer List对变量进行初始化后,才进入构造函数。Constructor Initializer List里忽略的成员变量(为空则相当于全部忽略),会由in-class initializers初始化,否则采取默认初始化进入构造函数体
构造函数体实际是给成员二次赋值,类里的class type成员其实已经默认初始化过了。所以C++ Primer里面讲如果编译器支持,推荐使用in-class initializers机制
对于built-in type,要么in-class initialization,要么initializer list,要么构造函数里赋值,总之不能不管
类的静态函数成员可以在类内部或者外部定义,而静态数据成员(const除外)则只能在外部定义以及初始化

class Sales_data {
    // 默认构造函数,负责调用in-class initializer初始化units_sold,以及默认初始化revenue
    Sales_data() = default;

    //bookNo由Constructor Initializer List初始化,units_sold由in-class initializer初始化
    //revenue默认(未)初始化
    Sales_data(const std::string &s): bookNo(s) { }
    std::string bookNo;             
    unsigned units_sold = 0;        //in-class initializer
    double revenue;
};

赋值的结果是左边的操作元,为左值,也就是说,下面的写法语法正确

int a = 0;
(a = 0) = 1;                    //the final value of a is 1.

需要注意的是,赋值操作符的优先级很低,该带括号的时候不能遗漏。
顺便提一下++i和i++的区别:前者将操作元增加,并且返回改变后的操作元;后者将操作数增加,返回原先值得拷贝作为结果。前置自增返回的结果是左值,后置自增返回的是右值。前置自增操作符做的无用功少,虽然C++编译器对int和指针类型的后置自增操作符作了优化,C++ Primer推荐如无特殊需求,优先使用前置自增操作符。
数组不支持拷贝初始化或者将一个整体赋值给另一个数组。

int a[] = {0,1,2}
int a2[] = a;           // error: cannot initialize one array with another
a2 = a;                 // error: cannot assign one array to another

new & delete(http://www.cnblogs.com/fly1988happy/archive/2012/04/26/2471099.html)

  1. new与operator new

C++中有很多语法让人难以理解,如:new operator(操作符,下同)和operator new之间差异,确切的说,应该是new与operator new 的区别。

1.1 new operator

如下代码:

string *ps=new string("memory management");

这里所使用的new就是所谓new operator,是由C++语言内建的,就像sizeof那样,不能改变意义,总是做相同的事情。

这个动作的含义分为两方面:

第一,它分配足够的内存,用来放置某类型的对象。对于上例而言,它分配足够放置一个string 对象内存。

第二,它调用一个构造函数,为刚才分配的内存中的那个对象设定初始值。

new operator总是做这两件事,无论如何你是不能改变其行为。

1.2 operator new

能够改变的是用来容纳对象的那块内存的分配行为,new operator调用某个函数,执行必要的内存分配动作,你可以重写或者重载那个函数,改变其行为。这个函数名称就叫operator new 。

函数 operator new 通常声明如下:

void * operator new (size_t size);

其返回类型void*。即返回一个指针,指向一块原始的、未设置初始值的内存。

函数中的size_t参数表示需要分配多少内存,你可以将operator new 重载,加上额外的参数,但第一个参数类型必须总是size_t。

或者你从来没有直接用过operator new ,但是你可以像调用任何其他函数一样地调用它。

void* rawMemory=operator new(sizeof(string));

这里的operator new 将返回指针,它指向一块足够容纳string对象的内存。

和malloc一样,operator new 的唯一任务就是分配内存,它不知道什么是构造函数,它只负责分配内存。

取得operator new 返回的内存并将之转为一个对象,是new operator的责任。

1.3 当编译器看到这个句子:

string *ps=new string("memory management");

它必须产生一些代码,或多或少会反映如下行为:

  1. void* memory=operator new(sizeof(string)); //取得原始内存,用于放置一个string对象

  2. call string::string("memory management") on *memory;//将内存中对象初始化

  3. string ps=static_cast<string>(memory); //让ps指向新完成的对象

注意第二步,调用一个构造函数。身为程序员没有权利绕过new operator像这么使用构造函数,但是编译器却是这么干的。

这就是为什么如果你想要做出一个heap-based object,一定要用new operator的原因。

也就是说new 出来的东西都放在heap里面,而无法直接调用“对象初始化所必须的构造函数”。

  1. placement new

2.1 有时候你真的会想直接调用一个构造函数,针对一个已经存在的对象调用其构造函数,并无意义,因为构造函数用来对象初始化,而对象只能初始化一次。但是你偶尔会有一些分配好的原始内存,你需要在上面构建对象,有一个特殊的地方 operator new 称为placement new,允许这么做。

例如:

class Widget { public: Widget(int widgetSize); ...... };

Widget* constructWidgetInBuffer(void *buffer,int size) { return new (buffer) Widget(size); }

此函数返回指针,指向一个Widget object,它被构造于传递给此函数的一块内存缓存区上。当程序运行到共享内存或者内存I/O映射。这类函数可能是有用的,因为在那样运用中,对象必须置于特定的地址,或者置于特殊函数分配出来的内存上。

2.2 函数内部

Widget* constructWidgetInBuffer 只有一个表达式new (buffer) Widget(size),

有点奇怪,其实不足为奇,这是new operator的用法之一,指定一个额外的自变量(buffer)作为new operator "隐式调用operator new "。于是,被调用的operator new 除了接受"一定要有size_t自变量"之外,还接受了一个void* 参数,指向一块内存,准备用来接受构造好的对象。这样的operator new 就是所谓的placement new :

void * operator new(size_t size,void* location)

{

return location;

}

operator new 的目的是要为对象找到一块内存,然后返回一个指针指向它,在placement new 的情况下,调用者已经知道指向内存的指针了,因为调用者知道对象应该放在哪里。因此placement new 唯一需要做的就是将它获得的指针再返回。

至于没有用到(但一定得有)的size_t参数,之所以不赋予名称,为的是避免"编译器某物未被使用"的警告。

另外注意:placement new 是C++标准程序库的一部分,要使用placement new 得用#include<new>,旧式编译器用 #include<new.h>

回头想想placement new ,我们便能了解new operator和operator new之间的关系。

两个术语表面上令人迷惑,但其实很好理解:

1)如果你希望将对象产生于heap,就是得new operator,它不但分配内存而为该对象调用一个构造函数。

2)如果你只是打算分配内存,请用operator new,就没有构造函数被调用。

3)如果你打算在heap object产生自己决定的内存分配方式,请写一个自己的operator new。并使用new operator,它将会自动调用你所写的operator new。

4)如果你打算在已经分配(并拥有指针)的内存构造对象,请使用placement new 。

  1. delete 与内存释放

为了避免resource leaks,每一个动态分配行为都必须匹配一个相应的释放动作。

3.1 函数 operator delete对于内建的delete operator(操作符)就好像 operator new 对于new operator一样。

string *ps;

...

delete ps; //使用delete operator.

内存释放动作是由operator delete执行的,通常声明如下:

void operator delete(void* memoryToBeDeallocated);

因此 delete ps;会造成编译器代码如下:

1)ps->~string();//调用析构函数

2)operator delete(ps);//释放对象所占用的内存

3.2 这里提示我们,如果只打算处理原始的、未设初值的内存,应该完全回避 new operator和delete operator。改为调用operator new取得内存并以operator delete归还系统。

例如:

void* buffer=operator new (50*sizeof(char));//分配内存,放置50个char,没有调用构造函数

...

operator delete(buffer); //释放内存,而没有直接调用析构函数。

这组行为类似malloc和free。

3.3 placement new

如果使用了placement new ,在某块内存中产生对象,你应该避免那块内存使用delete operator(操作符)。

因为delete operator会调用operator delete来释放内存,但是该内存所含的对象最初并不是由operator new 分配来的。placement new只是返回它接收的指针而已,谁知道那个指针从哪里来呢?

所以为了抵消该对象的构造函数的影响,使用placement new 时应该直接调用该对象的析构函数。

例如:

void * mallocShared(size_t size);//申请分配内存

void freeShared(void * momery);//释放内存

void* sharedMemory=mallocShared(sizeof(Widget));

Widget *pw=constructWidgetBuffer(sharedMemory,10);//使用前面Widget类的placement new

...

delete pw;//无定义,因为sharedMemory来自mallocShared,不是来自new。

pw->~Widget();//OK,析构函数pw所指Widget对象,但并未释放Widget所占用内存。

freeShared(pw);//OK,释放pw所指的内存,不调用任何析构函数。

如上述所示,如果交给placement new的原始内存(raw memory)本身是动态分配而得的,那么最终得释放那块内存,以避免memory leak。

  1. 动态分配数组(Arrays)

前面所做的都是基于单一对象上的,如果是一组对象呢?

string *ps=new string[10];//分配一个对象数组

4.1 这里的new 与前面的new 行为类似,但略有不同,这里不能再operator new分配内存,而是以operator new[]负责分配。

和operator new 一样,operator new[]也可以被重载。

注:operator new[]是相当晚的时候才加入C++的一个特性,所以你的编译器不一定能支持它。如果是这样,全局的operator new 会被用来为每一个数组分配内存(不论数组中的对象是什么类型)。在这样的编译器下定制“数组内存分配行为”很困难,因为你得改写全局的operator new才行。默认情况下,全局版的operator new 负责程序中所有的动态内存分配,所以其行为的任何改变都可能带来全局的影响。

另外,前面讲过,operator new 只允许size_t一个参数。所以你如果决定声明为自己的函数,你的程序便不兼容于任何做了相同决定的程序库。

多方面考虑之下,如果编译器不支持operator new[],定制数组内存管理行为,不是一个明智的决定。

4.2 数组的new 与单一对象的new所调用的构造函数不同,数组的new 必须针对数组中每一个对象调用一个构造函数。

string *ps=new string[10];//调用operator new[]以分配足够容纳10个string对象的内存,然后针对每个元素调用string的默认构造函数。

同样的,当使用了delete,它也会针对数组中每一个元素调用析构函数,然后再调用operator delete[]释放内存。

如:delete []ps;//为数组中的每一个元素调用string 析构函数,然后再调用 operator delete[] 释放内存。 (先调用析构函数,再释放内存。)

跟operator delete一样 operator delete[]也可以被重载。

最后小结一下,new 和delete都是内建的操作符,语言本身所固定了,无法重新定制。但它所调用的内存分配/释放的函数,即operator new和operator delete可以被重载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容