👇内容速览👇

基本语言篇

C++和C的区别

  1. C是一个结构化语言,它的重点在于算法和数据结构。C++,首要考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。

面向对象和面向过程

  • 面向过程是分析问题所需的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
  • 面向对象是把构成问题的事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
  1. C++对C的拓展
  • 双冒号::作用域运算符

代码中对同一个变量多次声明,在代码块中使用时,局部变量会将全局变量隐藏。若在代码块使用变量前添加::,表示为全局变量。

::表示作用域运算符,如常见的std::coutstd::endl;等,表示coutendlstd作用域下的标识符。

  • 命名空间namespace

主要用来解决命名冲突的问题,如多个人开发的不同模块中使用了相同的变量名和函数名,fatal error LNK1169:找到一个或多个重定义的符号,这时可以使用命名空间,将不同的模块分隔开。

  • 全局变量检测增强

C语言会忽略对全局变量重定义的检测,但不会忽略对局部变量的检测,C++中都会报错:error C2086: "nt a": 重定义

  • struct增强

    • C中strcut中不能有函数,C++中可以有,并且与class的区别在于是否有私有成员,和是否有构造函数;
    • 通过如下方式声明struct时,C语言定义使用结构体时必须使用struct,C++可以不用。
      struct Person{
          int a;
      };
      struct Person myperson; //C
      Person myperson; //C++
      
  • bool类型增强

C语言中没有bool类型,C++中有bool类型,其中sizeof(bool)=1

static关键字的作用

  • static修饰全局变量:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。加了static之后,其他目标文件不可见,只在当前文件中可见。不会产生重定义错误。
  • 修饰局部变量时:使它放在.data 或者.bss段,默认初始化为0,初始化不为0放在.data段,没有初始化或初始化为0放在.bss段。程序一运行起来就给他分配内存,并进行初始化,也是唯一一次初始化。它的生存期为整个源程序,程序结束,它的内存才释放。但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
  • 修饰普通函数时:和修饰全局变量一样。函数经过编译产生一个函数符号,被static修饰后,就变为local符号,不参与符号解析,只在本文件中可见。
  • 修饰类的成员变量时:就变成静态成员变量,不属于对象,而属于类。不能在类的内部初始化,类中只能声明,定义需要在类外。类外定义时,不用加static关键字,只需要表明类的作用域。
  • 修饰类的成员函数时:变成静态成员函数,也不属于对象,属于类。形参不会生成this指针,仅能访问类的静态数据和静态成员函数。调用不依赖对象,所以不能作为虚函数。用类的作用域调用。

static变量放在头文件会产生什么问题

如果在头文件中定义static变量,被多个文件引用,编译可以顺利通过!即该头文件被包含了多少次,这些变量就定义了多少次。

const关键字的作用

  • const 常量:定义时就初始化,以后不能更改。
  • const 形参:func(const int a){};该形参在函数里不能改变
  • const修饰类成员函数:该函数对成员变量只能进行只读操作

作用:

  1. 阻止一个变量被改变(使用mutableconst_cast可以解除const属性)
  2. 声明常量指针和指针常量
  3. const修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。

常量指针:指向常量的指针。不能通过这个指针来修改某变量的值。

const int *PtrConst;
int const *PtrConst;两者等价。

方便理解:

我们都知道在一级指针int *ptr中,ptr是一个指针变量,而*ptrptr指向某变量的值。因此,我们可以分析指针常量的声明如下:

从右往左看,PtrConst是一个指针变量,*PtrConst是指向某变量的值。而const作为常量修饰符,它修饰的是int * PtrConst或者*PtrConst,则认为const限定了PtrConst指向某变量的值。

指针常量:不能改变指向的常量(即不能改变指针指向的地址,但是该地址的内容可以改变)。

int *const ConstPtr=&a;//必须初始化,只能指向一个变量,绝不可再改变指向另一个变量。

如果同时定义了两个函数,一个带const,一个不带,会有什么问题

  • 当这两个函数作为普通的函数时,编译会报错,无法仅按返回类型区分两个函数
  • 当这两个函数作为类的成员函数时,是没有问题的

typedef和#define区别

  • typedef 仅限于为类型定义符号名称,#define 不仅可以为类型定义别名,也能为数值定义别名,比如您可以定义 1 为 ONE。
  • typedef 是由编译器执行解释的,#define 语句是由预编译器进行处理的。

const与#define区别

const是用于定义一个有类型的只读常变量。而#define是宏定义,是在编译期间对定义的变量进行直接文本替换,不做类型检查。变量定义时,大多数情况下const相对于#define来说有着更优的表现效果。基于以下几点原因:

  1. const对数据进行类型检查,#define不进行类型检查;
  2. 某些编译器支持对const对象进行调试,所有编译器都不支持对#define进行调试;
  3. const变量存放在内存的静态数据区域,所以在程序运行期间const变量只有一个拷贝,而#define修饰的变量则会在每处都进行展开,拥有多个拷贝;

其余区别:

  1. #define可以用来定义宏函数,而const不行。
  2. const除了可以修饰常变量外,还可以用于修饰指针、函数形参等等,修饰功能更强大。

define和inline的区别

  • 内联函数在编译时展开,宏在预编译时展开;
  • 内联函数直接嵌入到目标代码中,宏是简单的做文本替换;
  • 内联函数有类型检测、语法判断等功能,而宏没有;
  • inline函数是函数,宏不是;
  • 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义;

struct和class的区别

默认继承权限:如果不明确指定,来自class的继承安置private继承处理,来自struct的继承安置public继承处理。

成员的默认访问权限class的成员默认private权限,struct默认public权限

class这个关键字还用于定义模板参数,就像typename。但关键字struct不用于定义模板参数

union和struct的区别与联系

  • union (共用体):构造数据类型,也叫联合体

    用途:使几个不同类型的变量共占一段内存(相互覆盖)

  • struct (结构体):是一种构造类型

    用途: 把不同的数据组合成一个整体——自定义数据类型

主要区别:

  • structunion都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员; 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的,一个struct变量的总长度等于所有成员长度之和,遵从字节对其原则; 在union中,所有成员不能同时占用它的内存空间,它们不能同时存在 , union变量的长度等于最长的成员的长度。

  • 对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了,所以,共同体变量中起作用的成员是最后一次存放的成员; 而对于struct的不同成员赋值是互不影响的。

说一说violiate

来修饰被不同线程访问和修改的变量;volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接从内存读值

explicit的作用

explicit 关键字只能用于类内部的构造函数声明上。被修饰的构造函数的类,不能发生相应的隐式类型转换。

隐式类型转换: C++自动将一种类型转换成另一种类型,是编译器的一种自主行为。

说一说extern"C"

指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。

c++中四种cast转换

  • static_cast

    在编译期处理

    static_cast < type-id > ( expression )
    

    该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

    ① 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。

    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。

    ② 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

    ③ 把空指针转换成目标类型的空指针。

    ④ 把任何类型的表达式转换成void类型。

    注意:static_cast不能转换掉expressionconstvolatile、或者__unaligned属性。

  • const_cast

    在编译期处理

    const_cast<type_id> (expression)
    

    const_cast将转换掉表达式的const性质。

    该运算符用来修改类型的constvolatile属性。除了constvolatile修饰之外,type_idexpression的类型是一样的。

    • 常量指针被转化成非常量的指针,并且仍然指向原来的对象;
    • 常量引用被转换成非常量的引用,并且仍然指向原来的对象。
  • dynamic_cast

    在运行期,会检查这个转换是否可能。

    dynamic_cast<type_id> (expression)
    

    dynamic_cast 支持运行时识别指针或引用所指向的对象。

    该运算符把expression转换成type-id类型的对象。type-id必须是类的指针、类的引用或者void*;如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。

    dynamic_cast运算符可以在执行期决定真正的类型。如果downcast是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果downcast不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)。

    dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。

    • 在类层次间进行上行转换时,dynamic_caststatic_cast的效果是一样的;
    • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
    class B{
    public:
        int m_iNum;
        virtual void foo();
    };
    class D:public B{
        public:
        char *m_szName[100];
    };
    void func(B *pb){
        D *pd1 = static_cast<D *>(pb);
        D *pd2 = dynamic_cast<D *>(pb);
    }
    

    在上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;但是,如果pb指向的是一个B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针。

  • reinterpret_cast

    在编译期处理

    reinterpret_cast<type_id> (expression)
    

    该运算符把expression重新解释成type-id类型的对象。对象在这里的范围包括变量以及实现类的对象。

    此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。

C 指针指向的是物理地址吗?

C/C++的指针是指向逻辑地址

以Windows平台为例,任何一个C++程序肯定是运行在某一个进程中,Windows的32位系统对每一个用户进程都管理着一个寻址范围为4GB的地址空间, 各个进程的地址空间是相互独立的,很显然这是一个逻辑的地址空间,C++指针指向进程内的一个逻辑内存地址,然后由操作系统管理着从逻辑地十到物理地址的映射。

strcat,strcpy,strncpy,memset,memcpy,strlen的实现

  • strcat

    strcat 函数要求 dst 参数原先已经包含了一个字符串(可以是空字符串)。它找到这个字符串的末尾,并把 src 字符串的一份拷贝添加到这个位置。如果 srcdst的位置发生重叠,其结果是未定义的。

    char *strcat (char * dst, const char * src)
    {
        assert(NULL != dst && NULL != src);   // 源码里没有断言检测
        char * cp = dst;
        while(*cp )
            cp++;                      /* find end of dst */
        while(*cp++ = *src++) ;         /* Copy src to end of dst */
        return( dst );                  /* return dst */
    }
    
  • strcpy

    char *strcpy(char *dst, const char *src)    // 实现src到dst的复制
    {
        if(dst == src) return dst;              //源码中没有此项
       assert((dst != NULL) && (src != NULL)); //源码没有此项检查,判断参数src和dst的有效性
      char *cp = dst;                         //保存目标字符串的首地址
      while (*cp++ = *src++);                 //把src字符串的内容复制到dst下
      return dst;
    }
    
  • memcpy

    void *memcpy(void *dst, const void *src, size_t length)
    {
        assert((dst != NULL) && (src != NULL));
      char *tempSrc= (char *)src;            //保存src首地址
      char *tempDst = (char *)dst;           //保存dst首地址
      while(length-- > 0)                    //循环length次,复制src的值到dst中
          *tempDst++ = *tempSrc++ ;
      return dst;
    }
    
  • strncpy

    char *strncpy(char *dst, const char *src, size_t len)
    {
        assert(dst != NULL && src != NULL);     //源码没有此项
        char *cp = dst;
        while (len-- > 0 && *src != '\0')
            *cp++ = *src++;
        *cp = '\0';                             //源码没有此项
        return dst;
    }
    
  • memset

    将参数a所指的内存区域前length个字节以参数ch填入,然后返回指向a的指针。在编写程序的时候,若需要将某一数组作初始化,memset()会很方便。

    void *memset(void *a, int ch, size_t length)     
    {     
        assert(a != NULL);     
        void *s = a;     
        while (length--)     
        {     
            *(char *)s = (char) ch;     
            s = (char *)s + 1;     
        }     
        return a;     
    }
    
  • strcmp

    字符串比较

    int mystrcmp(const char *dest,const char *src)
    {  
         int i=0;
       //判断str1与str2指针是否为NULL,函数assert的头文件为#include<assert.h>
       assert(dest!=NULL && src !=NULL);  //[1]    
    
        //如果dest > source,则返回值大于0,如果dest = source,则返回值等于0,如果dest  < source ,则返回值小于0。  
       while (*dest && *src && (*dest == *src)) 
       {    
         dest ++;  
         src ++; 
       }
       return *dest - *source; [2]
    }
    
  • strlen

    int strlen(const char *StrDest)
    {
        int i;
        i=0;
        while((*StrDest++)!='\0')
        { 
            i++;
        }//这个循环体意思是从字符串第一个字符起计数,只遇到字符串结束标志'\0’才停止计数
        return i;
    }
    

++i和i++的区别与实现

区别

  1. i++ 返回原来的值,++i 返回加1后的值。
  2. i++ 不能作为左值,而++i 可以。

实现

// 前缀形式:++i
int& int::operator++() //这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
{//函数本身无参,意味着是在自身空间内增加1的
  *this += 1;  // 增加
  return *this;  // 取回值
}
//后缀形式:i++
const int int::operator++(int) //函数返回值是一个非左值型的,与前缀形式的差别所在。
{//函数带参,说明有另外的空间开辟
  int oldValue = *this;  // 取回值
  ++(*this);  // 增加
  return oldValue;  // 返回被取回的值
}

全局变量与全局静态变量

  1. 若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
  2. 若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。
  3. 具有外部链接的静态,可以在所有源文件里调用,除了本文件,其他文件可以通过extern的方式引用。

声明和定义的区别?

  1. 变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。
  2. 变量声明:用于向程序表明变量的类型和名字。
  3. 定义也是声明:当定义变量时我们声明了它的类型和名字。
  4. extern关键字:通过使用extern关键字声明变量名而不定义它。

函数也有声明和定义,但由于函数的声明和定义是有区别的,函数的定义是有函数体的,所以函数的声明和定义都可以将extern省略掉,反正其他文件也是知道这个函数是在其他地方定义的。

int a[10],求sizeof(a)和sizeof(a*)

sizeof(a) = sizeof(int)*10;
sizeof(*a);//就是指针的大小

int (*a)[10] 解释(指针数组)

  • int *a[10] :数组指针。数组a里存放的是10个int型指针
  • int (*a)[10] :a是指针,指向一个数组。此数组有10个int型元素

int *a[10]

先找到声明符a,然后向右看,有[]说明a是个数组,再向左看,是int *,说明数组中的每个元素是int *。所以这是一个存放int指针的数组。

int(a)[10]

先找到声明符a,被括号括着,先看括号内的(优先级高),然后向右看,没有,向左看,是,说明a是个指针,什么指针?在看括号外面的,先向右看,有[] 是个数组,说明a是个指向数组的指针,再向左看,是int,说明数组的每个元素是int。所以,这是一个指向存放int的数组的指针。

指针和引用

  1. 指针有自己的一块空间,而引用只是一个别名

  2. 使用sizeof,一个指针大小是4,而引用则是被引用对象的大小

  3. 指针可以初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用

  4. 使用参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象

  5. 指针在使用中可以指向其他对象,而引用只能是一个对象的引用

  6. 指针可以有多级指针,而引用只有一级

  7. 指针和引用使用++运算符的意义不一样。对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。

    指针自加,比如 int a[2] = {0,10}int *pa =apa++表示指针往后移动一个int的长度。指向下一个内存地址。即pa从指向a[0]变成指向a[1]引用是值++;比如b是引用a[0]的,++表示a[0]的值++从0变为1;

    int a=0;
    int b=&a;
    int *p=&a;   
    b++;//相当于a++;b只是a的一个别名,和a一样使用。
    p++;//后p指向a后面的内存
    (*p)++;//相当于a++
    
  8. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能会引起内存泄漏

  9. 引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

指针传递和引用传递

  • 指针从本质上讲是一个变量,变量的值是另一个变量的地址,指针在逻辑上是独立的,它可以被改变的,包括指针变量的值(所指向的地址)和指针变量的值对应的内存中的数据(所指向地址中所存放的数据)。

  • 引用从本质上讲是一个别名,是另一个变量的同义词,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化(先有这个变量,这个实物,这个实物才能有别名),而且其引用的对象在其整个生命周期中不能被改变,即自始至终只能依附于同一个变量(初始化的时候代表的是谁的别名,就一直是谁的别名,不能变)。

指针参数传递本质上是值传递,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

函数指针和指针函数

  • 指针函数,简单的来说,就是一个返回指针的函数,其本质是一个函数,而该函数的返回值是一个指针。

    声明格式为:*类型标识符 函数名(参数表)
    
  • 函数指针,其本质是一个指针变量,该指针指向这个函数。总结来说,函数指针就是指向函数的指针。

    声明格式:类型说明符 (*函数名) (参数)
    

    如下:

    int (*fun)(int x,int y);
    

    函数指针是需要把一个函数的地址赋值给它,有两种写法:

    fun = &Function;
    fun = Function;
    

    取地址运算符&不是必需的,因为一个函数标识符就表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表。

    调用函数指针的方式也有两种:

    x = (*fun)();
    x = fun();
    

编译与底层篇

C++源文件从文本到可执行文件的过程

1. 预处理(产生.i文件)

g++ -E helloworld.cpp -o helloworld.i
  • #define删除,并将宏定义展开
  • 处理一些条件预编译指令,如#ifndef,#ifdef,#elif,#else,#endif(作用:防止重复包含头文件)等。将不必要的代码过滤掉。
  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。这个过程是递归进行的,因为被包含的文件也包含其他文件
  • 过滤掉所有注释里面的内容
  • 添加行号和文件名标识
  • 保留#program编译器指令,因为编译器需要使用他们

2. 编译(产生.s文件)

g++ -S helloworld.i -o helloworld.s

编译就是将预处理的文件进行一系列的词法分析、语法分析、语义分析,以及优化后产生相应的汇编代码文件。

3. 汇编(产生.o或.obj文件)

汇编过程实际上是把汇编语言代码翻译成目标机器指令的过程,即生成目标文件。目标文件中所存放的也就是与源程序等效的目标机器语言代码。目标文件由段组成,通常一个目标文件至少有两个段:

  • 代码段:该段中所包含的主要是程序的指令,该段一般是可读和可执行的,但一般不可写。
  • 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读、可写、可执行的。

UNIX环境下主要有三类目标文件:

  • 可重定位文件:其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
  • 共享的目标文件:这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
  • 可执行文件:它包含了一个可以被操作系统创建一个进程来执行之的文件。 汇编程序生成的实际上是第一种类型的目标文件,对于后两种还需要其他的处理才能得到,这个就是连接程序的工作了。

4. 链接(产生.out或.exe文件)

链接就是把每个源代码独立的编译,然后按照它们的要求将它们组装起来,链接主要解决的是源代码之间的相互依赖问题,链接的过程包括地址和空间的分配,符号决议,和重定位等这些步骤。

静态链接/库

在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应的链接方式称为静态链接。

静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

静态库的缺点在于:浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件

动态链接/库

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

解释一下.so文件

  • .o文件是源码编译出的二进制文件
  • .a文件实质上就是.o文件打了个包。一般把它叫做静态库文件。它在使用的时候,效果和使用.o文件是一样的。
  • .so文件就不一样了,它不是简单的.o文件打了一个包,它是一个ELF格式的文件,也就是linux的可执行文件。
  • .so文件可以用于多个进程的共享使用(位置无关的才行),所以又叫共享库文件。程序在使用它的时候,会在运行时把它映射到自己进程空间的某一处,其不在使用它的程序中。

静态库和动态库的区别

  1. 静态链接库的后缀名为lib,动态链接库的导入库的后缀名也为lib。不同的是,静态库中包含了函数的实际执行代码,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息;
  2. 由于静态库是在编译期间直接将代码合到可执行程序中,而动态库是在执行期时调用DLL中的函数体,所以执行速度比动态库要快一点;
  3. 静态库链接生成的可执行文件体积较大,且包含相同的公共代码,造成内存浪费;
  4. 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
  5. DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性,适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。

头文件是否参与编译

会参与预编译

头文件不用被编译。我们把所有的函数声明全部放进一个头文件中,当某一个.cpp源文件需要它们时,它们就可以通过一个宏命令 #include包含进这个.cpp文件中,从而把它们的内容合并到.cpp文件中去。当.cpp文件被编译时,这些被包含进去的.h文件的作用便发挥了。

如何防止头文件重复编译

  1. #ifndef,#ifdef,#elif,#else,#endif
  2. #pragma once

除了#pragma once是微软编译器所特有的之外,用宏和#pragma once的办法来避免重复包含头文件,主要区别在于宏处理的方法会多次打开同一头文件,而#pragma once则不会重复打开,从而#pragma once能够更快速。

#pragma once指定当前文件在构建时只被包含(或打开)一次,这样就可以减少构建的时间,因为加入#pragma once后,编译器在打开或读取第一个 #include 模块后,就不会再打开或读取随后出现的同 #include 模块。

include头文件的顺序以及双引号""和尖括号的区别

预处理器发现 #include指令后,就会寻找后面跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的 #include 指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。但是包含头文件有两种方式,尖括号和双引号。

  • 尖括号:表示编译器只在系统默认目录或尖括号内的工作目录下搜索头文件,并不去用户的工作目录下寻找,所以一般尖括号用于包含标准库文件,例如:stdio.h,stdlib.h
  • 双引号:表示编译器先在用户的工作目录下搜索头文件,如果搜索不到则到系统默认目录下去寻找,所以双引号一般用于包含用户自己编写的头文件。

main函数在执行前和执行后有哪些操作

main函数执行之前,主要就是初始化系统相关资源

  1. 设置栈指针
  2. 初始化static静态和global全局变量,即data段的内容
  3. 将未初始化部分的全局变量赋初值:数值型shortintlong等为0boolFALSE,指针为NULL,等等,即.bss段的内容
  4. 全局对象初始化,在main之前调用构造函数
  5. main函数的参数,argcargv等传递给main函数,然后才真正运行main函数

main函数执行之后

  1. 全局对象的析构函数会在main函数之后执行;
  2. 可以用 _onexit 注册一个函数,它会在 main 之后执行;

C语言是怎么进行函数调用的?

什么是栈帧?

每一次函数调用都为这次函数调用开辟一块空间,这个空间就叫做栈帧。 每个函数的每次调用,都有它自己独立的一个栈帧。

首先必须明确一点也是非常重要的一点,栈是向下生长的,所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中

  • 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。
  • 寄存器esp(stack pointer)可称为“ 栈指针”。

要知道的是:

  • ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。
  • esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。

假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述:

  1. 先将调用者A的堆栈的基址ebp入栈,以保存之前任务的信息。
  2. 然后将调用者A的栈顶指针esp的值赋给ebp,作为新的基址(即被调用者B的栈底)。
  3. 然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。
  4. 函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶esp,使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebpesp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。

来分析下面的程序,下面的程序的功能就是调用swap函数交换两个数的值,然后返回两个数的差。

void swap(int *a,int *b)  
{  
   int c;  
   c = *a;   
   *a = *b;  
   *b = c;  
}  
  
int main(void)  
{  
   int a ;  
   int b ;  
   int ret;  
   a =16;  
   b = 64;  
   ret = 0;  
   swap(&a,&b);  
   ret = a - b;  
   return ret;  
}

下面是这个过程对应的栈结构

可以看出,在调用swap函数后,会为更改栈指针和帧指针的指向,为swap分配一个栈帧结构。

C语言参数压栈顺序?

C函数的参数压栈顺序是从右到左

printf函数的原型是:int printf(const char *format,...);

没错,它是一个不定参函数,那么我们在实际使用中是怎么样知道它的参数个数呢?这就要靠format了,编译器通过format中的%占位符的个数来确定参数的个数。

现在我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了!!

而如果把参数从右到左压栈,情况又是怎么样的?函数调用时,先把若干个参数都压入栈中,再压format,最后压pc,这样一来,栈顶指针加2便找到了format,通过format中的%占位符,取得后面参数的个数,从而正确取得所有参数。

C语言如何处理返回值?

函数在返回返回值的时候汇编代码一般都会将待返回的值放入eax寄存器暂存,接着再调用mov指令将eax中返回值写入对应的变量中。 函数在调用结束后不会会自动清理栈上的内存,如果我们再次访问原来栈空间上的数据,那么我们读取到的数据还是可以读取到的。

return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值 函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。

编译器如何识别函数重载

C++将会对重载的函数进行名称修饰或者叫名称矫正

int fun(int a)
int fun(float b)

这样的重载函数 在编译器下就可能是?fun@@YXX?fun@@XXY这样的进行了貌似无意义的修饰 用于编译器的识别。

库函数和系统调用区别

  • 系统调用

    系统调用指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。它通过软中断向内核态发出一个明确的请求。系统调用实现了用户态进程和硬件设备之间的大部分接口。

  • 库函数

    库函数用于提供用户态服务。它可能调用封装了一个或几个不同的系统调用(printf调用write),也可能直接提供用户态服务(atoi不调用任何系统调用)。

共享内存相关

Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。

1. 新建共享内存shmget

int shmget(key_t key,size_t size,int shmflg);
  • key:共享内存键值,可以理解为共享内存的唯一性标记。
  • size:共享内存大小
  • shmflag:创建进程和其他进程的读写权限标识。
  • 返回值:相应的共享内存标识符,失败返回-1

2. 连接共享内存到当前进程的地址空间shmat

void *shmat(int shm_id,const void *shm_addr,int shmflg);
  • shm_id:共享内存标识符
  • shm_addr:指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。
  • shmflg:标志位
  • 返回值:指向共享内存第一个字节的指针,失败返回-1

3. 当前进程分离共享内存shmdt

int shmdt(const void *shmaddr);

4. 控制共享内存shmctl

和信号量的semctl函数类似,控制共享内存

int shmctl(int shm_id,int command,struct shmid_ds *buf);
  • shm_id:共享内存标识符
  • command: 有三个值
  • IPC_STAT:获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
  • IPC_SET:设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。
  • IPC_RMID:删除共享内存
  • buf:共享内存管理结构体。

C++内存模型

堆 heap

  • 用来存放进程运行中被动态分配的内存段。由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”

栈 stack

  • 是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。 存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。 存放程序中的局部变量(但不包括static声明的变量,static变量放在数据段中)。同时,在函数被调用时,栈用来传递参数和返回值。由于栈先进先出特点。所以栈特别方便用来保存/恢复调用现场。

全局/静态存储区 (.bss段和.data段)

  • 全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。

常量存储区 (.rodata段)

  • 存放常量,不允许修改(通过非正当手段也可以修改)

代码区 (.text段)

  • 存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)

malloc的内存分配方式,另外brk系统调用和mmap系统调用的作用分别是什么?

malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。当进行内存分配时,malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。

搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。

首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。

下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。

最佳适配:对堆进行彻底的搜索,从头开始,遍历所有块,使用数据区大小大于size且差值最小的块作为此次分配的块。

合并空闲块 在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,我们需要将两个相邻的内存块进行合并。

new和malloc的区别

1. 返回类型安全性

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void指针转换成我们需要的类型。

2. 内存分配失败时的返回值

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL*malloc分配内存失败时返回NULL

3. 是否需要指定内存大小

使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。

4.属性

new/deleteC++关键字,需要编译器支持。malloc/free是库函数,需要C头文件支持。

5. new建立的是一个对象,malloc分配的是一块内存。

free之后的指针使用有什么问题

free(str)后指针仍然指向原来的堆地址,即你仍然可以继续使用,但很危险,因为操作系统已经认为这块内存可以使用,他会毫不考虑的将他分配给其他程序,于是你下次使用的时候可能就已经被别的程序改掉了,这种情况就叫“野指针”,所以最好free()了以后再置空 str = NULL; 即本程序已经放弃再使用他。

如果物理内存是2G 如果mallco 4G可以么?会有什么问题?

malloc的实现与物理内存自然是无关的,分配到的内存只是虚拟内存,而且只是虚拟内存的页号,代表这块空间进程可以用,实际上还没有分配到实际的物理页面。

new具体是怎么开辟内存的

简单数据类型(包括基本数据类型和不需要构造函数的类型)

  • 简单类型直接调用operator new分配内存;
  • 可以通过new_handler来处理new失败的情况;
  • new分配失败的时候不像malloc那样返回NULL,它直接抛出异常。要判断是否分配成功应该用异常捕获的机制;

复杂数据类型(需要由构造函数初始化对象)

  • new复杂数据类型的时候先调用operator new,然后在分配的内存上调用构造函数。

C/C++内存问题有哪些

  • 内存重复释放(一般在出现double free时基本上都是这个原因)
  • 内存泄漏。申请的内存忘了释放。
  • 内存越界使用
  • 内存未分配成功确使用了它
  • 内存分配成功却没有初始化就使用了内存
  • 使用了无效指针
    • 已经释放对象,却继续操作改指针所指的对象
      • 程序当中的对象调用关系过于复杂,是在难以搞清哪个对象是否已经释放了内存,从根本上解决对象管理混乱的情况。
      • 函数的return语句写错了,注意不要返回指向“栈内存”的指针或者引用。char * getString(){char b[] = "Hello, Tocy!"; return b;}
      • 使用free或者delete释放之后,没有将其置空,导致产生野指针。
    • 多线程中某一动态分配的对象同时被两个线程使用,一个线程释放了该对象,另一个线程却继续对该对象进行操作

什么时候会发生段错误

段错误是指程序访问(读写)了系统未给予读写权限的内存空间。包括:访问了不存在的内存空间,访问了系统保护的空间,对只读内存空间写覆盖等。

分为以下几种情况:

  1. 使用野指针
  2. 试图修改字符串常量的内容
  3. memory leak,内存泄漏

内存泄漏和内存溢出的区别?

  • 内存溢出

系统已经不能再分配出你所需要的空间,比如你需要100M的空间,系统只剩90M了,这就叫内存溢出。

比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。

  • 内存泄漏

用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出。

向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。

定位内存泄露

1. 查看进程maps表

在实际调试过程中,怀疑某处发生了内存泄漏,可以查看该进程的maps表,看进程的堆或mmap段的虚拟地址空间是否持续增加。如果是,说明可能发生了内存泄漏。如果mmap段虚拟地址空间持续增加,还可以看到各个段的虚拟地址空间的大小,从而可以确定是申请了多大的内存。

2. 重载new/delete操作符

重载new/delete操作符,用list或者map记录对内存的使用情况。new一次,保存一个节点,delete一次,就删除节点。 最后检测容器里是否还有节点,如果有节点就是有泄漏。也可以记录下哪一行代码分配的内存被泄漏。 类似的方法:在每次调用new时加个打印,每次调用delete时也加个打印。

3. top 指令

在Linux上面可以快速定位泄漏的程序和程度。

4. matrace

mtrace的原理是记录每一对malloc-free的执行,若每一个malloc都有相应的free,则代表没有内存泄露; 对于任何非malloc/free情況下所发生的内存泄露问题,mtrace并不能找出来。

栈溢出和堆溢出的区别?

  • 堆溢出:不断的new 一个对象,一直创建新的对象,但是没有delete
  • 栈溢出:死循环或者是递归太深,递归的原因,可能太大,也可能没有终止。

堆和栈的区别(从数据结构和内存方面)

数据结构中的堆和栈

就像装数据的桶或箱子,是一种具有后进先出性质的数据结构。

像是一颗倒立的大树,堆是一种经过排序的树形数据结构,每个节点都有一个值。通常我们所说的堆的数据结构是指二叉树。堆的特点是根节点的值最小(或最大),且根节点的两个树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意的,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

内存中的堆和栈

堆栈空间分配

栈:由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆: 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

堆栈缓存方式

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 
{ 
    int b;char s[] = "abc";char *p2;char *p3 = "123456"; //123456\0在常量区,p3在栈上。 
    static int c =0; 全局(静态)初始化区 
    p1 = (char *)malloc(10);  堆 
    p2 = (char *)malloc(20);}

区别 👇

1. 申请方式和回收方式不同

栈(satck):由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间。 堆(heap):需程序员自己申请(调用malloc,realloc,calloc),并指明大小,并由程序员进行释放。容易产生memory leak。

char  *p;
p = (char *)malloc(sizeof(char));

但是,p本身是在栈中。

由于栈上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。而堆上的数据只要程序员不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。

2. 申请后系统的响应

1)栈:只要栈的空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

2)堆:首先应该知道操作系统有一个记录空闲内存地址的链表,但系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free语句才能正确的释放本内存空间。另外,找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

说明:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题 堆会在申请后还要做一些后续的工作这就会引出申请效率的问题。

3. 申请效率

栈由系统自动分配,速度快。但程序员是无法控制。

堆是由malloc分配的内存,一般速度比较慢,而且容易产生碎片,不过用起来最方便。

4. 申请大小的限制

1)栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

2)堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

5. 堆和栈中的存储内容

1)栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

2)堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

6. 存取效率

堆:char *s1="hellow tigerjibo";是在编译是就确定的;

栈:char s1[]="hellow tigerjibo";是在运行时赋值的;用数组比用指针速度更快一些,指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上读取。

7. 分配方式

堆都是动态分配的,没有静态分配的堆。

栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的。它的动态分配是由编译器进行释放,无需手工实现。

如何限制一个类对象只在栈(堆)上分配空间?

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如A* ptr=new A;这两种方式是有区别的。

静态建立类对象: 是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。

动态建立类对象: 是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

只能在堆上分配类对象

当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。

因此,将析构函数设为私有,类对象就无法建立在栈上了。

class A  
{  
public:  
    A(){}  
    void destory(){delete this;}  
private:  
    ~A(){}  
};  

试着使用A a;来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。

上述方法的缺点:

无法解决继承问题

如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。 因此析构函数不能设为private。 还好C++提供了第三种访问控制,protected。 将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。

类的使用很不方便

使用new建立对象,却使用destory()函数释放对象,而不是使用delete()。 (使用delete()会报错,因为delete()对象的指针,会调用对象的析构函数,而析构函数类外不可访问。这种使用方式比较怪异。)

为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。

class A  
{  
protected:  
    A(){}  
    ~A(){}  
public:  
    static A* create()  
    {  
        return new A();  
    }  
    void destory()  
    {  
        delete this;  
    }  
}; 

这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。

只能在栈上分配类对象

只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。 虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。

因此,将operator new()设为私有即可禁止对象被new在堆上。

class A  
{  
private:  
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的  
    void operator delete(void* ptr){} // 重载了new就需要重载delete  
public:  
    A(){}  
    ~A(){}  
};  

对内存对齐的理解,为什么要内存对齐

CPU每次从内存中取出数据或者指令时,并非想象中的一个一个字节取出拼接的,而是根据自己的字长,也就是CPU一次能够处理的数据长度取出内存块,比如32位处理器将取出32位也就是4个字节的内存块进行处理。这里有一个问题:是只需要两个字节怎么办?答案是还是取出4个字节,然后内存处理器会帮忙完成数据挑拣在传送给CPU。

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐又分为自然对齐规则对齐

自然对齐指的是将对应变量类型存入对应地址值的内存空间,即数据要根据其数据类型存放到以其数据类型为倍数的地址处。例如char类型占1个字节空间,1的倍数是所有数,因此可以放置在任何允许地址处,而int类型占4个字节空间,以4为倍数的地址就有0,4,8等。编译器会优先按照自然对齐进行数据地址分配。

规则对齐以结构体为例就是在自然对齐后,编译器将对自然对齐产生的空隙内存填充无效数据,且填充后结构体占内存空间为结构体内占内存空间最大的数据类型成员变量的整数倍。

设计一下如何采用单线程的方式处理高并发

  • 可以采用IO复用模型,比如select,epoll来增加单线程的处理效率

  • 可以通过使用事件驱动型IO来异步处理事件。

机器为什么使用补码?

  1. 根据运算法则减去一个正数等于加上一个负数, 即: 1-1 = 1+(-1), 所以计算机被设计成只有加法而没有减法, 而让计算机辨别“符号位”会让计算机的基础电路设计变得十分复杂,于是就让符号位也参与运算,从而产生了反码
  2. 用反码计算, 出现了“0”这个特殊的数值, 0带符号是没有任何意义的。 而且会有[0000 0000]和[1000 0000]两个编码表示0。于是设计了补码, 负数的补码就是反码+1,正数的补码就是正数本身,从而解决了0的符号以及两个编码的问题: 用[0000 0000]表示0,用[1000 0000]表示-128。
  3. -128实际上是使用以前的-0的补码来表示的,所以-128并没有原码和反码。使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数。 这就是为什么8位二进制, 使用补码表示的范围为[-128, 127]。

补码就是最方便的方式。它的便利体现在,所有的加法运算可以使用同一种电路完成。

数的原码表示形式简单,适用于乘除运算,但用原码表示的数进行加减法运算比较复杂,引入补码之后,减法运算可以用加法来实现,且数的符号位也可以当作数值一样参与运算,因此在计算机中大都采用补码来进行加减法运算

负数二进制表示

在计算机中,

  • 正数是直接用原码表示的,如单字节5,在计算机中就表示为:0000 0101
  • 负数以其正值的补码形式表示,如单字节-5,在计算机中表示为1111 1011

原码

  • 一个正数的原码,是按照绝对值大小转换成的二进制数;
  • 一个负数的原码,是按照绝对值大小转换成的二进制数,然后最高位补1。

比如 :

00000000 00000000 00000000 00000101是 5的 原码。

10000000 00000000 00000000 00000101是 -5的 原码。

反码

正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。

取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)

比如:

正数00000000 00000000 00000000 00000101的反码还是00000000 00000000 00000000 00000101

负数10000000 00000000 00000000 00000101每一位取反(除符号位),得11111111 11111111 11111111 11111010

称:11111111 11111111 11111111 1111101010000000 00000000 00000000 00000101的反码。 反码是相互的,所以也可称: 10000000 00000000 00000000 0000010111111111 11111111 11111111 11111010互为反码。

补码

正数的补码与原码相同

负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1

比如:

10000000 00000000 00000000 00000101的反码是:11111111 11111111 11111111 11111010

那么,补码为:

11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011

所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。转换为十六进制:0xFFFFFFFB

字节序的概念?

首先,要明确以下两点:

  • 双字节数据以上有高字节和低字节之分
  • 字节在内存中从低地址到高地址依次存放

这样,以字WORD(双字节)数据0x1234为例:

  • 大端字节序:字数据高字节存储在内存的低地址,而低字节存储在内存中的高地址 。如0x12存储在地址a处,则0x34存储在a+1处(即:高对低,低对高
  • 小端字节序:字数据高字节存储在内存的低地址,而低字节存储在内存中的高地址 。如0x34存储在地址a处,则0x12存储在a+1处(即:高对高,低对低
//int--char的强制转换,是将低地址的数值截断赋给char,利用这个准则可以判断系统是大端序还是小端序
#include <iostream>
using namespace std;
int main()
{
    int a = 0x1234;
    char c = static_cast<char>(a);
    if (c == 0x12)
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}