码迷,mamicode.com
首页 > 编程语言 > 详细

C++之中this指针与类的六个默认函数小结

时间:2016-06-02 14:20:39      阅读:224      评论:0      收藏:0      [点我收藏+]

标签:

       我们先来看看this指针。之前看过一篇关于this指针的文章,觉得写的很好,今天决定自己来写一写,顺便总结一下C++里面关于类的一些内容。

       什么是this指针呢?简单的说它是一个指向类的实例的指针,就好像当我们在进入一个房子之后,可以看见房子里的桌子,椅子、地板等, 但是看不到房子的全貌。对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this是一个指针,它时时刻刻指向这个实例。

来看看this指针的特性:

1)this指针的类型是一个类类型 * const, 这表示什么呢?如果你想在你的成员函数之中改变你的this指针的指向,很显然,做不到!

2)不过当你在使用sizeof操作符的时候千万要注意,不要把this指针的大小考虑进去,这是为什么呢?this指针本身不占用大小,它并不是对象的一部分,因此不会影响sizeof的结果。

3)this指针是类成员函数的第一个默认隐含参数,因此不需要你显示地传入,你在类成员函数之中可以直接使用this指针。

4)this指针的作用域是在非静态函数的内部,在成员函数(非静态)开始前构造,在成员函数(非静态)结束后消除(下面会分析为什么是在非静态函数内部才能使用)

5)this指针的传入方式,这里不得不提函数的调用约定_thiscall,如果参数确定,那么this指针是通过ecx传递给被调用者的,若参数不确定,那么this指针在所有参数压入之后在压入。

(_thiscall的调用约定如下:

这 是 C++ 语言特有的一种调用方式,用于类成员函数的调用约定。如果参数确定,this 指针存放于 ECX 寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。__thiscall 不是关键字,程序员不能使用。参数按照从右至左的方式入栈。


6)this指针不能再初始化列表之中使用,原因是在初始化列表之中,类的对象还没有创建,编译器不知道对象的结构,因此不知道应该为其分配多大的空间。

接下来就进入C++之中关于类的六个默认函数。先来看一张图:
技术分享

我们就按顺序说起:
                                                                                       一、构造函数

       什么是构造函数呢?但凡想要理解一个事物,必须要从它的定义入手。构造 函数它是一个特殊的成员函数,1)他没有返回值。2)它的名字与类的名字相同。3)创建类类型对象的时候由编译器自动去调用。4)在对象的生命周期之内且 只调用一次,从而保证每一个都有一个合适的初始值。

再来看一下构造函数的特性:

       首先构造函数允许重载, 最为常见的构造函数的重载就是我们所熟知的拷贝构造函数。不但如此,你也可以显示的定义你的构造函数,如:Date(){} 里面可以什么也不写,这就是一个最简单的构造函数(什么也不写的构造还函数,你可以去看汇编代码,还是做了些事情的),只要你满足重载的条件(不过要注意 会不会产生二义性),你可以定义多个构造函数(通过参数可以选出你所要调用的构造函数)。当然如果你什么也没有写,那么编译器就会帮你自动生成一个构造函 数,当然这个构造函数也不会帮你做什么。

       其次构造函数允许定义缺省构造函数, 比如上面定义的那个什么也不写的构造函数就是一个缺省构造函数,缺省构造函数可以分为两种,第一种就是参数列表之中什么也不写的,称为无参的构造函数。第 二中就是参数列表之中的每一个参数都对应有一个默认值,称为全缺省构造函数。但是要注意的是这两个构造函数之能显示的给出一个,否则如果你什么参数也不 写,两个构造函数都可以调用,但是编译器不知道到底应该调用哪一个,因此会产生二义性问题。

       还有就是构造函数不允许使用const修饰,因为构造函数需要改变参数的内容,所以它是要修改参数(this指针指向的对象中的内容)。

       最后一点就是关于默认函数可以使用初始化列表, 但是要注意初始化列表是按照类中原有的对象的顺序进行初始化的,与变量在初始化列表中的写的顺序无关,因此特别要注意最好不要用变量来初始化另一个变量, 当然如果你的参数没有全部在初始化列表中去初始化,那么,没有出现的变量也会初始化(初始化为0xcccccccc),千万不要以为只有初始化列表里列出 来的 成员变量才在执行构造函数体之前进行初始化的。事实上,即使没有在初始化列表中出现,所有成员变量仍然是在初始化这一步骤(也就是在执行构造函数的函数体 前)完成初始化的。所以,我们常常在构造函数的函数体内对变量进行初始化,实际是非常浪费和降低效率的。应该养成用初始化列表进行赋初值的习惯。

        顺便在这里提一下什么情况下使用初始化列表呢?有三种情况必须使用初始化列表

1)非静态const数据成员(即没有static修饰的)

2)引用数据成员

3)类类型对象(该类之中没有缺省构造函数)

最后说一下构造函数的作用:1)构造对象   2)初始化对象   3)类型转化

                                                                            默认构造函数

       上面其实也提到了默认构造函数,其实默认构造函数就是,如果你没有显示的定义一个构造函数,那么编译器就会帮你生成一个构造函数。不过需要注意的是,如果 你这个生成的默认构造函数什么也不做,那么编译器会对其进行优化,你会发现你无法在汇编之中看到代码,那并不是没有产生默认构造函数,只不过是编译器进行 了优化。(比如你的一个Date类的对象之中有一个time类的对象,而time类对象有缺省构造函数的时候,哪怕你在Date类之中没有构造函数u,编 译器会给你生成一个默认构造函数,而且你可以在汇编之中看到,因为这个默认构造函数是有意义的)

       还有一点要说的就是,只要你显式定义了构造函数,即使该构造函数什么也不做,编译器也不会为该类合成默认的构造函数编译器生成的默认构造函数使用与变量初始化相同的规则来初始化成员,具有类类型的成员通过运行各自的默认构造函数来进行初始化。内置和符合类型的成员如指针、数组,只对定义在全局作用域中的对象初始化,当对象定义在局部作用域时,内置和符合类型的成员不进行初始化。在某些情况下,默认构造函数是由编译器隐式使用的。

                                                                    二、拷贝构造函数   

       只有单个形参,而且该形参是对本类类型对象的引用(常用const修饰),这样的构造函数称为拷贝构造函数。拷贝构造函数是特殊的构造函数,创建对象时使 用已存在的同类对象来进行初始化,由编译器自动调用。要注意的是,拷贝构造函数参数传递的是引用,如果参数不是引用,那么如果采用值传递,那么在传递过程 之中又会产生一个临时变量,而这个临时变量的产生又需要使用拷贝构造函数,于是一个无限递归问题就产生了。
      关于拷贝构造其实没什么好多说了,重点是知道什么失手我们使用到了拷贝构造函数。下面来看看一些使用到了
拷贝构造函数的场景。

1)利用对象进行传参的时候,如:

    void Fun(const Date date)  
    {}  

2)利用对象实例化另一个对象的时候,如:

    Date d1(1999, 1, 1);  
    Date d2(d1);  

3)利用参数作为返回值的时候,有时候会返回一个类的对象。


                                                                       三、析构函数

        析构函数,顾名思义,与构造函数的功能相反,析构函数是在对象被销毁时,由编译器自动调用,完成类的一些资源清理和汕尾工作。

来看一看析构函数的特点:

1)一个类里面只有一个,对象被销毁的时候只调用一次。

2)不能有参数,也不能有返回值,因此析构函数不能重载

3)如果没有显示的给出,编译器会默认生成一个。

4)在对象生命周期结束的时候,由编译器自动调用。

5)析构函数在函数体内并不是删除对象,而是做一些清理工作。这里有一点不得不提一 下:用delete或free来销毁对象时,会调用其析构函数,并将所占的全局堆内存空间返回。但从销毁对象到程序退出该作用域,对象的指针还存在于栈 中,并指向对象本来的位置。显然,这种情况下调用指针是非常危险的。Win32平台下访问这种指针,结果有三种可能情况:访问违例、取得无意义值、取得其 他对象。第一种会导致进程崩溃,后两种虽然不会立即崩溃,但是可能会有不可预测的行为操作或造成对象不必要的变化,需要谨慎避免。

6)析构顺序:利用栈的特性,先进后出,所以对后进的对象先进行析构,与构造函数生成对象的顺序相反。

                                                                  四、运算符的重载(后面三个默认函数)

      在这里提到了运算符重载,于是我把后面的三个重载放在一起讲。先来了解一下什么叫做运算符重载(或者叫做操作符重载),重载操作符是具有特殊函数名的函 数,关键字operator后面接需要定义的操作符符号。操作符重载也是一个函数,具有返回值和形参表。它的形参数目与操作符的操作数目相同,函数调用操 作符可以接受任意数目的操作数。

(在这里还要说明一下,一般来说,我们在操作符重载的时候,既可以将其写成一个类的友元函数,同样也可以将其写成一个类的成员函数。这里具体情况具体分析,一般来说,像+、-、*、/我习惯于写成友元函数,而像++、--之类的我习惯于写成类的成员函数,一般将算术操作符定义为非成员函数(友元函数),将赋值运算符定义成员函数

格式:返回类型 operate 操作符(参数列表);

 可以被重载的操作符:

技术分享

 不可以被重载的操作符:

技术分享

 1、不能通过连接其他符号来创建新的操作符:比如operator@;
   void operator @(){}

2、重载操作符必须有一个类类型或者枚举类型的操作数

	1. int operator +(const int _iNum1 , const int _iNum2 )   // 报错  
	2. {  
	3.     return ( _iNum1 + _iNum2);  
	4. }  
	5.   
	6. typedef enum TEST {one ,two ,three };  
	7. int operator+(const int _iNum1 , const TEST _test )  
	8. {  
	9.      return _iNum1;  
	10. }  


3、用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义

4、重载前后操作符的优先级和结合性是不变的


5、不在具备短求值特性重载操作符不能保证操作符的求值顺序,在重载&&和||中,对每个操作数
   都要进行求值,而且对操作数的求值顺序不能做规定,因此:重载&&、 ||和逗号操作符不是好的做法。

6、作为类成员的重载函数,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参。

    CTest operator+(const CTest test1, const CTest test2)const   // 报错  
    {  
         return test1;  
    }  
      
    CTest operator+(const CTest test1)const  
    {  
         return test1;  
    }  
7、一般将算术操作符定义为非成员函数,将赋值运算符定义成员函数
8、操作符定义为非类的成员函数时,一般将其定义为类的友元
9、== 和 != 操作符一般要成对重载
10、下标操作符[]:一个非const成员并返回引用,一个是const成员并返回引用
11、解引用操作符*和->操作符,不显示任何参数
13、自增自减操作符
    前置式++/--必须返回被增量或者减量的引用
    后缀式操作符必须返回旧值,并且应该是值返回而不是引用返回
14、输入操作符>>和输出操作符<<必须定义为类的友元函数

【建议】
   使用重载操作符,可以令程序更自然、更直观,而滥用操作符重载会使得类难以理解,在实践中很少发生明显的操作符重载滥用。但有些程序员会定义 operator+来执行减法操作,当一个重载操作符不明确时,给操作符取一个名字更好,对于很少用的操作,使用命名函数通常比用操作符好,如果不是普通 操作,没有必要为简洁而用操作符。

        

顺便在这里分析一下static、const、extern的相关内容。

                                                                         “诡异”的static

        首先来看static,同样的先来看看static是什么?static是一个关键字,被他修饰的变量称为静态变量,声明为static的类成员(成员数据或成员函数)称为类的静态成员。

来看一看static的特性

1)static在类中只是声明,必须要在类外初始化,并且要加上类的作用域(此时不需要再带上static关键字)。

2)静态成员为所有类对象所共享,不属于某个具体的实例。存放于静态区。

3)类静态成员即可用类名::静态成员或者对象.静态成员来访问。

4)类的静态成员函数没有默认的this指针,因此在它里面不能使用任何非静态成员。

5)静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值,const修饰符等参数。

6)静态成员函数调用方式为_cdcal。

7)静态成员函数不能访问非静态成员变量,同样也不能调用非静态成员函数,原因是静态 成员函数里面不能使用this指针,如果你非要访问,那么你可以把对象作为参数传进来,利用这个对象去访问非静态成员变量。(但是非静态成员函数却可以调 用静态成员函数,因为本质上静态成员函数还是一个成员函数,可以通过this指针去访问到它。)

                                                                      “严格”的const

        再来看一看const,还是先从定义入手,如const int a 这句语句,在C++之中表示a是一个常量,但是在C语言中则表示a是一个不可修改的变量。简单的说就是给一个变量赋予常属性。

再来看一看const的特性

1)const修饰形参,一般和引用同时使用。(一般只出现在类的赋值函数中,目的是为了实现链式表达
2)const修饰返回值。(这里有必要说一下:若函数的返回值是指针,且用const修饰,则函数返回值指向的内容是常数,不可被修改,此返回值仅能赋值给const修饰的相同类型的指针。如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。)
3)const修饰类数据成员,必须在构造函数的初始化列表中初始化。
4)const修饰类成员函数,实际修饰隐含的this,表示在类中不可以对类的任何成员进行修改。
5)在const修饰的成员函数中要对类的某个数据成员进行修改,该数据成员定义声明是必须加mutable关键字。
6)c语言中const修饰的变量为不可修改的变量。

来看一下下面一些const使用场景问题:

1.const对象可以调用非const成员函数和const成员函数吗?非const对象可以调用非const成员函数和const成员函数吗?

       这是不可以的,const类型使用的时候能做的有限,可以理解为权利较小,但是非const修饰的对象权利较大,可能一不小心就改了里免得内容,因 此,const对象只能访问const成员函数。因为const对象表示其不可改变,而非const成员函数可能在内部改变了对象,所以不能调用。而非 const对象既能访问const成员函数,也能访问非const成员函数,因为非const对象表示其可以改变。

2.const成员函数内可以调用其它的const成员函数非const成员函数吗?非const成员函数内可以调用其它的const成员函数非const成员函数吗?
       同样的与上面的情况类似,const成员函数表示里面的内容不能修改,权利很小,但是非const权利较大,可能会修改掉const成员函数里面的内容, 因此 const成员函数是不会改变类的数据成员的值的 但是非const 成员 函数是会改变的 因此 const 成员 函数是不能调用 非const 成员的。只能调用const成员函数。而非const成员函数既能调用const成员函数,也能调用非const成员函数,因为非const对象表示其可以改变。

                                                                          

                                                                           “神奇”的extern

        老样子,还是从定义入手,extern在源文件A里定义的函数,在其它源文件里是看不见的(即不能访问)。为了在源文件B里能调用这个函数,应该在B的头部加上一个外部声明


1. 声明外部实体

声明外部全局变量或对象,一般用于头文件中,表示在其它编译单元内定义的变量,链接时进行外部链接,如:
extern int ivalue;
此时的extern是必须的,省略了extern编译器将视为定义而不是声明,一般地在源代码中定义变量并进行初始化,在头文件中使用extern声明变量。

类似地用于声明外部全局函数,表示该函数在其它编译单元中定义,如:
extern void func( void );此时的extern可以省略。

extern   函数原型;  
  这样,在源文件B里也可以调用那个函数了。  
  注意这里的用词区别:在A里是定义,在B里是声明。一个函数只能(也必须)在一个源文件里被定义,但是可以在其它多个源文件里被声明。定义引起存储分配, 是真正产生那个实体。而声明并不引起存储分配。打一个粗俗的比方:在源文件B里声明后,好比在B里开了一扇窗,让它可以看到A里的那个函数。

 

#include "stdafx.h"
  1.extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
  2.如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有区别:
  extern int f(); 和int f();
  ================================
  如果定义函数的c/cpp文件在对应的头文件中声明了定义的函数,那么在其他c/cpp文件中要使用这些函数,只需要包含这个头文件即可。
  如果你不想包含头文件,那么在c/cpp中声明该函数。一般来说,声明定义在本文件的函数不用“extern”,声明定义在其他文件中的函数用“extern”,这样在本文件中调用别的文件定义的函数就不用包含头文件
  include “*.h”来声明函数,声明后直接使用即可。
  ================================
  举个例子:
  

 //extern.cpp内容如下:  
    
  // extern.cpp : Defines the entry point for the console application.  
  //  
    
  #i nclude "stdafx.h"  
  extern print(char *p);  
  int main(int argc, char* argv[])  
  {  
   char *p="hello world!";  
   print(p);  
   return 0;  
  }  
  //print.cpp内容如下  
  #i nclude "stdafx.h"  
  #i nclude "stdio.h"  
  print(char *s)  
  {  
   printf("The string is %s/n",s);  
  }  


  结果程序可以正常运行,输出结果。如果把“extern”去掉,程序依然可以正常运行。
  
  由此可见,“extern”在函数声明中可有可无,只是用来标志该函数在本文件中定义,还是在别的文件中定义。只要你函数在使用之前声明了,那么就可以不用包含头文件了。
  
    VC++6.0中常出现的"unexpected end of file while looking for precompiled header directive"的问题?

    如何解决:"fatal error C1010:VC++6.0中常出现的"unexpected end of file while looking for precompiled header directive"的问题?

    我想大家在VC6.0中经常回遇到这样的问题,如何解决呢?

 1、看看是否缺少“;”,“}”  
 如:类,结构体后面的分号
 隐藏得深的是宏、.h文件的问题就要费点心思了

 2、一定是你在类的部分定义被删除了,M$在每个类中定义一些特殊的常量,是成对的,如下:

    .h:  
    #if !defined(AFX_CHILDFRM_H__54CA89DD_BA94_11D4_94D7_0010B503C2EA__INCLUDED_)  
    #define AFX_CHILDFRM_H__54CA89DD_BA94_11D4_94D7_0010B503C2EA__INCLUDED_  
    .......  
    //{{AFX_INSERT_LOCATION}}  
    // Microsoft Visual C++ will insert additional declarations immediately before the previous line.  
      
    #endif // !defined(AFX_MAINFRM_H__54CA89DB_BA94_11D4_94D7_0010B503C2EA__INCLUDED_)   

 你可以新建一个类,然后把这些拷贝过去或补上就可以了。  
 3、在头部加入 #include "stdafx.h"

 4、在CPP文件第一行加上#include "stdafx.h"。
 或者Rebuild All. 

 5、

 (1). [Project] - [Settings] - [C/C++] - [Category]
 (2). 选择 [Precomplied Headers]
 (3). 单选 [Not Using Precomplied Headers]
 (4). [OK]


 如果以上不能解决问题,那么就请看以下内容.引起这样的错误,有可能你只是增加了一个.H和.CPP的文件.这时你就要按上面所说.
名含"stdafx.h"即可.如果还要在多个文件里同时使用结构类型,你就要继续向下看了.一定会有不少收获的.

 类型的定义和类型变量的定义不同,
 类型定义只是描述一个类型,
 是给编译器看的,
 不会产生可执行代码。
 变量定义是指在执行文件中真实得存在这么一块内容。

 因为每个.c里都要写清楚类型定义很麻烦,
 所以一般都把类型定义写在.h里
 ,而在.c里采用简单的写法,如struct A a;
 这样定义变量,
 不需把整个类型的描述再写一遍。

 ------------------------------------------------------------------------
 所以,struct类型定义放到 XX.h里面,
 XX.cpp 里加struct str st_r;
 XXXXX.cpp加上#i nclude "XX.h"
 然后直接使用extern struct str st_r;


2. 声明函数的编译和链接方式

extern 后可以跟”C”或”C++”用于声明全局函数的编译和链接方式,例如:
extern “C” void add( int a, int b);
extern “C++” void sum(int* ia, int leng);
void sum(int* ia, int leng);
其中的extern “C++”可以省略,它是在C++中默认的链接方式,即后面两种声明方式是等效的。这种声明有两种含义:首先,声明这些函数使用外部链接方式,其实现不在 本编译单元之内;另一种含义,则是告诉编译器编译方式,如extern “C”则是告诉编译器使用C语言的编译方式编译该函数。

C++支持函数重载,所以参数不同在编译后生成的函数名也不同,如:
int max(int a, int b);
int max(float a, float b);
在编译时生成的函数名可能分别为_max_int_int、_max_float_float,通过在函数名后加上参数类型来区分不同的函数,如果使用C 语言方式,则生成的函数名中不包含参数信息,只生成_max,所以无法实现重载,也就是说在extern “C”中不能出现函数名重载,例如:

extern “C”{
int max(int a, int b);
int max(float a, float b);
}


非法,编译器将报错。而C++标准中并没有定义extern “C”与extern “C++”的具体实现方式,不同编译器生成的符号规则可能不同。

需要注意的是,如果函数声明使用了extern “C”,则函数定义必须使用C编译器编译,或者使用extern “C”来修改函数的编译方式,一般地将extern “C”声明的函数的定义所在的源程序扩展名使用.c即可,而C++代码放在.cpp文件中。如果将extern “C”声明的函数实现也放在.cpp中,则需要使用extern “C”来声明函数编译方式,例如:
extern “C” {
int max( int a, int b) { return a > b ? a : b; }
}


只有在C++中使用C语言的库或者两种语言混合编程的时候才会用到extern “C”,而在C语言中是不支持extern “C”的,所以为了头文件通用,需要使用宏来控制,例如:
#ifndef MAX_H // 防止重复引用
#define MAX_H
#ifdef __cplusplus
extern "C" {
#endif
int max (int a, int b);
#ifdef __cplusplus
}
#endif
#endif
其中__cplusplus为C++定义的宏,凡是C++的编译器都定义了该预编译宏,通过它来检测当前编译器是否使用的是C++编译器。

C++之中this指针与类的六个默认函数小结

标签:

原文地址:http://blog.csdn.net/loving_forever_/article/details/51547371

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!