C++语法易错点

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MOU_IT/article/details/87940853

1、#include <*.h>和 #include “*.h”的区别:

    #include <*.h>不会在当前目录中搜索头文件,而#include “*.h”会在当前目录中搜索。#include “*.h”的搜索顺序为:

    ①先搜索当前目录;
    ②再搜索gcc的环境变量CPLUS_INCLUDE_PATH(C程序使用的是C_INCLUDE_PATH);
    ③最后搜索gcc的内定目录 :/usr/include、/usr/local/include 等;

2、int &*p、int *& p和int **p的区别:

    int  &*p: 是不正确的,*与p靠得最近,所以p是一个指针,但是这个指针的类型是int &,所以这个是错吴的, 因为不能创建引用的指针。
    int  *& p:是正确的, &与p靠得最近,所以p是一个引用,这个引用的类型是int*,也就是一个指针变量的引用,但是必须初始化,否则也是错误的。
    int  **p :是正确的,*与靠的最近,所以p是一个指针,而这个指针指向的是int*,所以这个一个指向指针的指针。

int i,*p = &i;
int **q = &p;    //指向指针的指针。

3、指针数组和数组指针的区别(char * str[] 和 char (*str)[])的区别:

     1)char * str[ ]中str相与“[]”先结合,就成了str[],这是什么?数组!没错,这就是数组,接下来str[]在于“*”结合,就成了*str[],所以这个式子就可以这样写:char *(str[]);这就是指针数组,什么是指针数组?指针数组就是数组里面装的是指针,你可以这样来理解,整形数组里面装的是整形数据,那么指针数组里面装得就是指针,指针数组其实就是二维数组。
     2)对于char (*str)[ ],由于“()”优先级最高所以先运算“()”里的内容,*str,这是指针,然后在于“[]”结合,这就是数组指针,什么是数组指针?数组指针就是指向数组首地址的指针,你也可以这样来理解,int *p;p指向的是int类型数据的地址,那么数组指针呢?就是指向数组首地址的指针。总结:指针数组是数组,数组的内容是指针;数组指针是指针,指针指向的值数组的首地址。数组指针是指向一维数组的指针,也称行指针,专门用来指向二维数组 。

4、sizeof()和strlen()的区别:

    1)strlen()是函数,在运行时才能计算。参数必须是字符型指针(char*),且必须是以’\0’结尾的。当数组名作为参数传入时,实际上数组已经退化为指针了。它的功能是返回字符串的长度,不包括'\0'。 
    2)sizeof()是运算符,而不是一个函数,在编译时就计算好了,用于计算数据空间的字节数。因此,sizeof不能用来返回动态分配的内存空间的大小。sizeof常用于返回类型和静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。sizeof()返回的长度包括'\0'。

5、静态库(.a库)和动态库(.so库)的区别:

   1) .a文件 :静态库文件,静态库在编译时已经被链接到目标代码中,运行程序不依赖该静态库文件; 
      优点:将程序使用的函数的机器码复制到最终的可执行文件中,提高了运行速度;如果库函数改变,整个程序需要重新编译 
      缺点:所有需用到静态库的程序都会被添加静态库的那部分内容,使得可执行代码量相对变多,占用内存和硬盘空间
   2).so文件:动态库文件,在程序运行的时候载入动态库文件,程序要正常运行必须依赖于它需要使用的动态库文件; 
      优点:只有在程序执行的时候, 那些需要使用的函数代码才被拷贝到内存中。动态库在内存中可以被被多个程序使用,又称之为共享库,节约了内存和磁盘空间,以时间效率去换取空间效率;当调用接口没改变,库文件发生改变重新编译,而调用的主程序可不用重新编译; 
      缺点:运行速度不及静态库文件; 

6、C++的垃圾回收和智能指针

    java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存,而且这两个问题针对的内存区域就是Java内存模型中的堆区。垃圾回收是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。垃圾回收机制的引入可以有效的防止内存泄露、保证内存的有效使用,也大大解放了Java程序员的双手,使得他们在编写程序的时候不再需要考虑内存管理。
    对于C/C++的程序,常会遇到诸如程序运行时突然退出,或占用的内存越来越多,最后不得不定期重启的一些典型症状。这些问题的源头可以追溯到C/C++中的显式堆内存管理上(C++中使用new/delete进行分配和回收,C中使用malloc/free进行分配和回收)。通常情况下,这些症状都是由于程序没有正确处理堆内存的分配与释放造成的,从语言层面来讲,我们可以将其归纳为以下的一些问题:

    1)野指针:一些内存单元已被释放,之前指向它的指针却还在被使用。这些内存有可能被运行时系统重新分配给程序使用,从而导致无法预测的错误。如使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
    2)重复释放:程序试图去释放已经被释放过的内存单元,或者释放已经被重新分配过的内存单元,就会导致重复释放错误。通常重复释放内存会导致C/C++运行时系统打印出大量错误及诊断信息。
    3)内存泄漏:不再需要使用的内存单元如果没有释放就会导致内存泄漏。如果程序不断地重复进行这类操作,将会导致内存占用剧增。

      在堆上分配内存很容易造成内存泄漏,这是C/C++的最大的“克星”,如果你的程序要稳定,那么就不要出现MemoryLeak。所以,我还是要在这里千叮咛万嘱付,在使用malloc系统函数(包括calloc,realloc)时千万要小心。对于malloc和free的操作有以下规则: 

  1)配对使用,有一个malloc,就应该有一个free。(C++中对应为new和delete) 
  2)尽量在同一层上使用,不要malloc在函数中,而free在函数外。最好在同一调用层上使用这两个函数。 
  3)malloc分配的内存一定要初始化。free后的指针一定要设置为NULL。

    虽然显式的管理内存在性能上有一定的优势,但也被广泛地认为是容易出错的。随着多线程程序的出现和广泛使用,内存管理不佳的情况还可能会变得更加严重。因此,很多程序员也认为编程语言应该提供更好的机制,让程序员拜托内存管理的细节。在C++中,一个这样的机制就是标准库中的智能指针。在C++11新标准中,智能指针被进行了改进,以更加适应实际的应用需求。而进一步的,标准库还提供了所谓“最小垃圾回收”的支持。C++11标准中改用unique_ptrshared_ptrweak_ptr等智能指针来自动回收堆分配的对象。

7、C++的堆对象、栈对象

(1)C++对象初始化两种方法:

  在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。
     1)静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数;
     2)动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。参考博客

  第一种初始化方法:ClassName object(初始化参数);               //在Stack栈里面分配空间,自动释放。
  第二种初始化方法:ClassName object = new ClassName();     //在heap堆里面分配空间,要手动释放。

      栈对象的优势是在适当的时候自动生成,又在适当的时候自动销毁,不需要程序员操心;而且栈对象的创建速度一般较堆对象快,因为分配堆对象时,会调用 operator new操作,operator new会采用某种内存空间搜索算法,而该搜索过程可能是很费时间的,产生栈对象则没有这么麻烦,它仅仅需要移动栈顶指针就可以了。但是要注意的是,通常栈空间容量比较小,一般是1MB~2MB,所以体积比较大的对象不适合在栈中分配。特别要注意递归函数中最好不要使用栈对象,因为随着递归调用深度的增加,所需的栈空间也会线性增加,当所需栈空间不够时,便会导致栈溢出,这样就会产生运行时错误。
      堆对象产生时刻和销毁时刻都要程序员精确定义,也就是说,程序员对堆对象的生命具有完全的控制权。我们常常需要这样的对象,比如,我们需要创建一个对象,能够被多个函数所访问,但是又不想使其成为全局的,那么这个时候创建一个堆对象无疑是良好的选择,然后在各个函数之间传递这个堆对象的指针,便可以实现对该对象的共享。另外,相比于栈空间,堆的容量要大得多。实际上,当物理内存不够时,如果这时还需要生成新的堆对象,通常不会产生运行时错误,而是系统会使用虚拟内存来扩展实际的物理内存。

(2)栈和堆申请大小的限制 :

      栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,由编译器决定栈的大小(一般1M/2M),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 
      堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。参考博客。

8、sizeof()如何计算一个对象的大小(参考

    用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。注意:类只是一个类型定义,它本身是没有大小可言的。

(1)只有构造函数和析构函数的空类也要实例化,所谓类的实例化就是在内存中分配一块地址,编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。 而析构函数,跟构造函数这些成员函数,是跟sizeof无关的。
(2)给这个类添加成员变量,最后输出的大小就是这些成员变量的大小之和(这里涉及到一个成员对齐问题)。此外,静态变量在计算时是不做考虑的。
(3)若析构函数变成了虚函数,类的大小也变成了4字节,这是因为有了虚函数,编译器就会为类创建一个虚函数表(vtable),并创建一个指针(vptr)指向这个虚函数表。所以类大小变为4字节。如果在基类中再添加新的虚函数,该类的大小还是不会变,因为指向虚函数的指针是放在虚函数表中的,指向虚函数表的指针不会变。
(4)基类中已经存在虚函数时,如果派生类又声明了一个虚函数,虽然重新定义,但是因为它是从基类继承的,所以也继承了其虚函数表,并没有创新新的虚函数表。

    总结:一个类中,虚函数、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间。 

9、虚函数的作用和实现原理(参考

    C++中的虚函数的作用主要是实现了多态的机制。基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要再派生类中声明该方法为虚方法。当子类重新定义了父类的虚函数后,当父类的指针指向子类对象的地址时,[即B b; A a = &b;] 父类指针根据赋给它的不同子类指针,动态的调用子类的该函数,而不是父类的函数,且这样的函数调用发生在运行阶段,而不是发生在编译阶段,称为动态联编。而函数的重载可以认为是多态,只不过是静态的。注意,非虚函数,效率静态联编要比虚函数高,但是不具备动态联编能力。如果使用了virtual关键字,程序将根据引用或指针指向的 对 象 类 型 来选择方法,否则使用引用类型或指针类型来选择方法。

    编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。
    举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

1)如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。
2)如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

10、extern 关键字

    (1)extern 放在变量和函数前,表明该变量和函数在别处已有定义,在该行只是声明,并没有分配内存,此处是为了引用别处已定义好的变量函数,起到跨越文件使用变量和函数的作用,其实可以直接include相应的头文件来使用,主要是可以加快编译速度。比如:

a.cpp如下:
  int i = 1;

b.cpp如下:
#include <iostream>
using namespace std;
extern int i;
int main() {
    int a = i + 1;
    cout << a << endl;
}   

编译:g++ -O0 -std=c++11 -o run ./a.cpp b.cpp
运行:./run
结果:2  

    如果将b.cpp中extern去掉,则因为i在a.cpp中已经定义为了全局变量,当两个文件编译后进行链接时,会产生冲突,
报错如下:

/tmp/ccednFyK.o:(.data+0x0): multiple definition of `i'

    (2)extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程。

11、static关键字(指定变量或函数的作用域和存储的方式

    (1)static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)。局部静态变量使得该变量在退出函数后,不会被销毁,因此再次调用该函数时,该变量的值与上次退出函数时值相同。值得注意的是,生命周期并不代表其可以一直被访问,因为变量的访问还受到其作用域的限制。

void function()
{
    /*实际上nCount的初始化不是在函数体第一次执行时完成,而是在编译期其值就已经被
       确定,在main函数之前就完成了初始化,所以局部静态变量只会初始化一次*/
    static int nCount(0);    
    std::cout << "call function " << ++nCount << " times" << endl;
}

int main()
{
    for (int i = 0; i < 5; ++i) 
    {
        function();
    }
    return 0;
}

输出结果是:
call function 1 times
call function 2 times
call function 3 times
call function 4 times
call function 5 times

    (2)static修饰函数使得函数只能在包含该函数定义的文件中被调用。
    (3)static修饰全局变量,被static修饰的全局变量只能被该包含该定义的文件访问。
    (4)static修饰类的变量和函数,表明该变量和函数属于类,不再仅仅是类的实例对象。

12、const的关键字(const修饰的变量或对象的值是不能被更新的

    const修饰符可以把对象转变成常数对象,意思就是说利用const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用!任何修改该变量的尝试都会导致编译错误。因为常量在定以后就不能被修改,所以定义时必须初始化:对于类中的const成员变量必须通过初始化列表进行初始化。

(1)const修饰全局变量和局部变量;变量值不能修改;
(2)const引用是指向const对象的引用,不能改变const对象的值;
(3)const对象的动态数组:因为数组元素都是const对象,无法赋值。实现这个要求的唯一方法是对数组做值初始化;
(4)const修饰指针,const int *;指针的值不能修改;
(5)const修饰指针指向的对象, int * const;指针指向的对象不能修改;
(6)const修饰引用做形参;可以直接访问实参对象,并改变实参内容,而不是将实参复制给形参;
(7)const修饰成员变量,必须在构造函数列表中初始化,不能在类声明中初始化const数据成员;
(8)const修饰成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改。

13、const和static的区别(参考

(1)const定义的常量在超出其作用域之后其空间会被释放,而static定义的静态常量在函数执行后不会释放其存储空间。
(2)static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
(3)在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化,如:double Account::Rate=2.25; static关键字只能用于类定义体内部的声明中,定义时不能标示为static。
(4)在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
const数据成员,只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。
(5)const数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static const。
(6)const成员函数主要目的是防止成员函数修改对象的内容。即const成员函数不能修改成员变量的值,但可以访问成员变量。当方法成员函数时,该函数只能是const成员函数。
(7)static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1)不能直接存取类的非静态成员变量,调用非静态成员函数;2)不能被声明为virtual。

14、volatile的作用(告诉编译器不进行优化)

    volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会做减少存取内存的优化,读取变量时先从寄存器读取,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。

15、new和malloc的区别(参考

1)new分配内存按照数据类型进行分配,malloc分配内存按照大小分配;
2)new不仅分配一段内存,而且会调用构造函数进行初始化,但是malloc只开辟内存空间,不进行初始化。calloc会进行初始化。
3)new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化;
4)new是一个操作符可以重载,malloc是一个库函数;
5)new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会;
6)malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作;
7)new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。因此对于new,正确的姿势是采用try…catch语法,而malloc则应该判断指针的返回值。
8)new和new[]的区别,new[]一次分配所有内存,多次调用构造函数,分别搭配使用delete和delete[],同理,delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

15、C++的内存分配(参考

(1)堆:那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
(2)栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)自由存储区:由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
(4)全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
(5)常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

16、C++内存中堆和栈的区别(参考):

 (1)管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
(2)空间大小:一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。
(3)碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
(4)生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
(5)分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
(6)分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(如伙伴系统和slab算法)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

17、智能指针参考

    (1)什么是智能指针:在C++中,为了确保指针的寿命和其所指向的对象的寿命一致是件棘手的事,特别是当多个指针指向同一对象时。例如,为了让多个集合拥有同一对象,你必须把指向该对象的指针放进那些集合内,而且当其中一个指针被销毁时不该出现问题,也就是不该出现所谓的空悬指针(也叫野指针)或多次删除被指向对象,最后一个指针被销毁时也不该出现资源泄露问题。为了避免上述问题的一个通用做法是使用智能指针,智能指针能够知道它自己是不是指向某物的最后一个指针,并且运用这样的知识,在它的确是该对象的最后一个拥有者而且它被删除时,销毁它所指向的对象。

    (2)智能指针是怎么实现的:

    智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类(智能指针自身保存在栈中),用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。该类的具体实现参考如下:

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:
    SmartPtr(T *p);
    ~SmartPtr();
    SmartPtr(const SmartPtr<T> &orig);                 // 浅拷贝
    SmartPtr<T>& operator=(const SmartPtr<T> &rhs);    // 浅拷贝
private:
    T *ptr;
    // 将use_count声明成指针是为了方便对其的递增或递减操作
    int *use_count;
};

template<class T>
SmartPtr<T>::SmartPtr(T *p) : ptr(p)
{
    try
    {
        use_count = new int(1);
    }
    catch (...)
    {
        delete ptr;
        ptr = nullptr;
        use_count = nullptr;
        cout << "Allocate memory for use_count fails." << endl;
        exit(1);
    }

    cout << "Constructor is called!" << endl;
}

template<class T>
SmartPtr<T>::~SmartPtr()
{
    // 只在最后一个对象引用ptr时才释放内存
    if (--(*use_count) == 0)
    {
        delete ptr;
        delete use_count;
        ptr = nullptr;
        use_count = nullptr;
        cout << "Destructor is called!" << endl;
    }
}

template<class T>
SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig)
{
    ptr = orig.ptr;
    use_count = orig.use_count;
    ++(*use_count);
    cout << "Copy constructor is called!" << endl;
}

// 重载等号函数不同于复制构造函数,即等号左边的对象可能已经指向某块内存。
// 这样,我们就得先判断左边对象指向的内存已经被引用的次数。如果次数为1,
// 表明我们可以释放这块内存;反之则不释放,由其他对象来释放。
template<class T>
SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs)
{
    // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1,
    // 从而防止自身赋值”而导致的提早释放内存
    ++(*rhs.use_count);

    // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象
    if (--(*use_count) == 0)
    {
        delete ptr;
        delete use_count;
        cout << "Left side object is deleted!" << endl;
    }

    ptr = rhs.ptr;
    use_count = rhs.use_count;
    
    cout << "Assignment operator overloaded is called!" << endl;
    return *this;
}

    (3)shared_ptr、unique_ptr的区别:

1)shared_ptr实现共享式拥有,多个智能指针可以指向相同的对象,该对象和其相关资源会在“最后一个reference被销毁”时被释放。
2)unique_ptr实现独占式拥有或者严格拥有,保证同一个时间内只有一个智能指针可以指向该对象,你可以移交拥有权,它对于避免资源泄露——例如“new创建的对象后因为发生异常而忘记调用delete“特别有用。

18、构造函数和析构函数是否会抛出异常?

(1)构造函数可以抛出异常
(2)C++标准指明析构函数不能、也不应该抛出异常:
    C++异常处理模型是为C++语言量身设计的,更进一步的说,它实际上也是为C++语言中面向对象而服务的。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分:
  1) 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
  2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

19、构造函数和析构函数可以调用虚函数吗?

    构造函数和析构函数都不能调用虚函数。C++中派生类在构造时会先调用基类的构造函数再调用派生类的构造函数,析构时则相反,先调用派生类的析构函数再调用基类的析构函数。假设一个派生类的对象进行析构,首先调用了派生类的析构,然后在调用基类的析构时,遇到了一个虚函数,这个时候有两种选择:Plan A是编译器调用这个虚函数的基类版本,那么虚函数则失去了运行时调用正确版本的意义;Plan B是编译器调用这个虚函数的派生类版本,但是此时对象的派生类部分已经完成析构,“数据成员就被视为未定义的值”,这个函数调用会导致未知行为。实际情况中编译器使用的是Plan A,如果虚函数的基类版本不是纯虚实现,不会有严重错误发生。

20、内联函数和宏定义区别:

(1)宏定义在预编译的时候就会进行宏替换;
(2)内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
(3)内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
(4)使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a * b,这很危险,正确写法:#define MUL(a, b) ((a) * (b)).

21、内存对齐的原则:

(1)从0位置开始存储;
(2)变量存储的起始位置是该变量大小的整数倍;
(3)结构体总的大小是其最大元素的整数倍,不足的后面要补齐;
(4)结构体中包含结构体,从结构体中最大元素的整数倍开始存;
(5)如果加入pragma pack(n) ,取n和变量自身大小较小的一个。

22、指针的相关问题(都是泪)

(1)指针的四要素 :

  1)指针变量:表示一个内存地址,通常为逻辑地址(虚拟地址),与实际的物理地址还有一个映射关系;
  2)指针变量的长度:在WIN32下为4个字节;
  3)指针指向的变量:该内存地址空间下存放的变量, 具体内容可能是各种类型的变量;
  4)指针指向的变量的长度:以该内存地址空间开始的内存空间大小。

(2)指针移动问题:

  指针p++具体移动的字节数等于指针指向的变量类型大小;

(3)指针和引用的区别:

1)指针是一个实体,而引用仅是个别名;
2)引用使用时无需解引用(*),指针需要解引用;
3)引用只能在定义时被初始化一次,之后不可变,指针可变;
4)引用没有 const,指针有 const;
5)引用不能为空,指针可以为空;
6)“sizeof (引用)”得到的是所指向的变量(对象)的大小,而“sizeof (指针)”得到的是指针本身(所指向的变量或对象的地址)的大小;
7)指针和引用的自增(++)运算意义不一样;
8)从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

(4)什么是数组退化为指针(参考):

    在进行参数的传递时,数组引用可以帮助我们防止数组退化为指针,而这是我们在编程中很难注意到的问题。

#include <iostream>

void each(int int_ref[10])
{
  std::cout << sizeof(int_ref)<<std::endl;
  for(int i=0;i<10;i++)
     std::cout<<int_ref[i]<<" ";
  std::cout<<std::endl;
}

void each2(int (&int_ref)[10])
{
    std::cout<<sizeof(int_ref)<<std::endl;
    for(int i=0;i<10;i++)
        std::cout<<int_ref[i]<<" ";
   std::cout<<std::endl;
}

int main()
{
    int int_array[] = {1,2,3,4,5,6,7,8,9,10};
    each(int_array);      //问题1:sizeof()的值?
    each2(int_array);     //问题2:sizeof()的值?      
    return 0;
}

    当然,如果不去理会sizeof()的话,这两个函数的输出不会有任何的不同,他们都能够正确的输出array[]中的10个值,但当我们观察一下sizeof()的值就会发现很大的不同。
    问题1:输出4
    问题2:输出40
    从这我们就能看出,当array[]作为参数传递过去后,如果接收的参数是也是一个数组,那么它就会退化为一个指针,也就是我们常说的“数组就是一个指针”。当接收的参数是一个数组引用时,就会发现它还是保持了自己的原生态,即“数组仍然是一个数组”。这时,数组引用就起到了一个保护自己退化为一个指针的作用。 

(5)数组名和指针的区别(参考):

char d[5];
d="hell";  //不能直接赋值,只能一个个赋值。错误。vs2010提示表达式d必须是可修改的左值。但是如果是指针则可以

char *d;
d="hell"; //字符串常量后面会自动添加\0.

1、地址相同,大小不同,看下面代码:

   int arr[10];
   int* p=arr;
   cout<<arr<<endl;
   cout<<p<<endl;
   cout<<sizeof(arr)<<endl;//结果为40
   cout<<sizeof(p)<<endl;//结果为4(在数组名做函数参数时会退化为指针)

arr为数组名,p为指针。
第3、4行输出的值一样,也就是说arr和p都是数组的首地址。第5、6行的结果不一样,arr的大小是整个数组的大小,而p的大小是指针的大小。

2、都可以用指针作为形参,指针的形参当然是指针。数组的形参可以是数组,也可以是指针。下面代码印证了数组的形参可以是指针。

void fun(int* p){
    cout<<p[0]<<endl;
}

int main(){
   int arr[10]={0};
   int* p=arr;
   fun(arr);   
   return 0;
}

这点可以看出,数组名完全可以当成指针来用。

3、指针可以自加,数组名不可以
  int arr[10]={0};
  int* p=arr;
  arr++;
  p++;
当数组名自加时程序编译就会出错,从这点应该可以看出,数组名是一个常量(const 修饰)。

4、作为参数的数组名的大小和指针的大小相同(即在数组名做函数参数时会退化为指针)
void fun(int arr[]){
   cout<<sizeof(arr)<<endl;//结果为4
   arr++;//编译成功
}

arr的大小变为4、arr++成功编译可以确定,作为参数的arr已经完全变成了一个指针。为了不退化,我们可以使用引用解决问题。

1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;
2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量,不能改变自增、自减和赋值;
3)指向数组的指针则是另外一种变量类型(在WIN32平台下,长度为4),仅仅意味着数组的存放地址;
4)数组名作为函数形参(不加上引用&)时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
5)很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4个字节的平民。

(6)指针的类型转换(参考):

    大端和小端机的区别:高序字节存放在低地址成为大端,低序字节存放在低地址成为小端。当我们定义一个某类型指针p,后可以p++,p--,*p这些都与“指针记录了所指对象类型大小”有关,不然p++和p--  一次跳了多远?还有*p一次取了多长,都是由记录的类型大小决定的:如int *p=&i;那么++ 一次至少会跳int那么远即4个字节(连续存储下),*p一次也会从p所指向地址为起点取出int那么长(4字节)空间的二进制信息,并将这些二进制翻译成“指针内部存着的其所指向的类型”的类型。记住这种“感觉”:初始化指针就是:

1)记下所指对象的地址;
2)同时记下所指类型和类型大小;
3)对一个指针解引用(*p)就是从指针所指地址为起点,读出所指类型大小那么大的空间的二进制,然后将二进制翻译成所指类型;
4)而++,——操作就是向前或向后跳所指类型那么大小的空间。

int t = 65<<8;   
char *p = (char*)&t+1;   
cout<<(int)*p<<endl;  

    65左移了8位然后被给一个被赋值给整型t,然后定义了char*指针p,而后的强制转换就有些意思了,按照前面的感觉也就不难理解了:取了t的地址然后由于遇见(char*),p存储的类型原本应该是t的类型int,然而由于(char*)的出现p中存储的类型将变成char类型,大小也为char那么大即一个字节,而后+1,这时这一跳将因为目前对象是char类型,而只跳一个字节(8位),也就是说这时p将指向之前左移8位的65这个数所在内存那么下面的cout将如期望的一样输出65这个数。
    我们有了感觉之后似乎对指针间的强制类型转换也很清楚了:

1)转换后的指针中记录的地址依旧是转换前的地址
2)转换后的指针记下的所指类型和类型大小,为强制转换的类型和强制转换类型的大小。
3)*操作会依旧会以指针所指地址为起点,却取出转换后的指针所记录的类型大小那么大的空间的二进制,翻译成指针转换后的所指类型。
4)转换后的++,和--跳的距离以转换后的指针中存放的类型大小为依据。

    那么我们更有感觉了:只要指针间在“能转换的情况下”我们就可以转来转去,只要保证不对指针进行移位就可以保证指针所指向地址一直不变,转换仅仅转的是里面存的类型和类型大小,只要我们最后能转回原来的类型还可以如同最初时一样按原类型操作它。 

(7)void * 指针是什么(参考):

1)void指针是一种特别的指针 
      void *vp                        //说它特别是因为它没有类型,或者说这个类型不能判断出指向对象的长度 
2)任何指针都可以赋值给void指针 
      type *p; 
      vp=p;                            //不需转换,只获得变量/对象地址而不获得大小 
3)void指针赋值给其他类型的指针时都要进行转换 
      type *p=(type*)vp;        //转换类型也就是获得指向变量/对象大小 
4)void指针不能复引用 
      *vp                               //错误,因为void指针只知道,指向变量/对象的起始地址,而不知道指向变量/对象的大小(占几个字节)所以无法正确引用 
5)void指针不能参与指针运算,除非进行转换 
      (type*)vp++;                //vp==vp+sizeof(type)

(8)什么是指针的释放:

1) 释放该指针指向的内存,只有堆上的内存才需要我们手工释放,栈上不需要;

2) 将该指针重定向为NULL;

(9)const int * 、int * constconst int * const区别:

1)const int *p:首先,const修饰的是整个*p(注意,是*p而不是p),所以*p是常量,是不能被赋值的。其次,p前并没有用const修饰,所以p是指针变量。能被赋值重新指向另一个内存地址。

2)int * const p:指针p因为有了const的修饰,所以为指针常量,即,指针p不能修改了。整个*p前面没有const修饰,则*p为变量而不是常量,所以,可改变*p的值。

3)const int * cosnt p:声明该指针变量里面的内容不可改变,同时该内容指向的内容亦不可改变。

猜你喜欢

转载自blog.csdn.net/MOU_IT/article/details/87940853