笔试面试 题目

目录

四个类型转换符

向下转型以及dynamic_cast的使用

稳定排序与非稳定排序

虚函数表创建时机

单继承以及多继承下虚函数表的绑定

shared from this 与智能指针的陷阱

define(宏定义)与typede区别       

typedef的一些用法和注意点        

缓冲(buffer)与缓存(cache)

sizeof的使用

数组名与指向数组首元素的指针的区别

容器的erase函数

野指针悬空指针

构造函数为什么不能为虚函数

什么时候把基类的析构函数必须实现成虚函数?   

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

构造函数和析构函数抛出异常

虚函数能为内联函数吗?

虚函数的绑定以及继承问题

哪些函数不能为虚函数

重载,隐藏,覆盖 

32位与64位下 short int long 长度的区别

内存分区 

扩展: .data和.bss区以及分区的原因

内存泄漏

数组sizeof的注意点

int *f(int)、int(*f)(int)、int *a[4]、int (*a)[4] 

堆和栈的区别

static关键字

static的一些辨析

volatile关键字

extern关键字

explicit关键字

decltype关键字

final和override关键字

delete 和 default关键字

构造函数为什么不能为const属性

引用类型和const类型的数据为什么不能在构造函数内部初始化

类内不同数据的初始化方式

必须要用初始化列表初始化的情况

一个派生类构造函数的执行顺序

new和malloc的区别

malloc的底层实现(brk与mmap)

C 和 C++ 区别

C++ 和 Java 区别

TCP与UDP区别

父进程与子进程之间的关系

HTTP三次握手并且说明双方的序号

左值引用与右值引用

段错误

内联函数与宏定义的区别

NULL与nullptr

关于this指针

静态绑定与动态绑定

怎样判断两个浮点数的大小

字节内存对齐

为什么模板类一般都是放在一个H文件中

cout与printf区别

delete this 的后果

什么时候需要合成构造函数

什么时候需要合成拷贝构造函数

strcpy和memcpy的区别

数组指针和指针数组 的区别

const char * 与 string 之间的转换

拷贝构造函数为什么只能用引用来传递参数而不能用传值

关于回调函数

程序从代码到可执行文件所经历的过程

容器内部删除一个迭代器

vector释放空间

vector的reserve()和resize()方法之间有什么区别

map中[]与find的区别

哈希表中解决冲突的方法

RTTI       

指针问题 

function类模板

bind函数

*p++,*++p,(*p)++

结构体与类区别

指针和引用的区别

vector与list的区别


四个类型转换符

转换类型操作符    作用
const_cast<new_type>(expression)去掉类型的const或volatile属性
static_cast<new_type>(expression)无条件转换,静态类型转换,与c的强制转换一样
dynamic_cast<new_type>(expression)用于父子指针之间的向下转型
reinterpret_cast<new_type>(expression)expression必须是一个指针、引用、算术类型、函数指针。用于类型之间的强制转换。

向下转型以及dynamic_cast的使用

向上转型:Base * bptr = new Son; 将派生类指针转换为父类指针(把派生类指针的地址赋值给父类类型的指针)

向下转型:Son * sptr = new Base;将父类指针转换为派生类指针(把父类指针的地址赋值给派生类类型的指针)

向上转型是安全的,在程序中可以直接使用,而向下转型是不安全的,因为当基类指针指向的是基类对象,而将基类指针转为子类指针时,子类指针所能辐射到的范围仍然是原父类指针所能辐射到的范围如果通过转换后的子类指针访问子类的专有成员,就会造成内存错误,因为实际指向的是基类对象,而基类对象中不存在这些成员。

所谓“安全的向下转型”即只有当Base class的指针确实指向Derived class对象时(即父指针多态)才能将其转为Derived class的指针。但是我们知道Base class的指针指向的对象类型在执行期时才能确定的,所以要想保证向下转型的安全性,就必须在执行期对Base class的指针进行查询看看它所指向对象的真正类型。

dynamic_cast运算符可以在执行期确定指针指向的真正类型(前提是Base class中要有虚函数)。当对Base class的指针向下转型时,如果向下转型时是安全的(也就是父类指针指向一个派生类对象),则这个运算符传回相应的子类指针,如果向下转型是不安全的,则这个运算符传回0(NULL)。

当对Base class的引用向下转型时,如果向下转型时是安全的(也就是父类指针引用一个派生类对象),则这个运算符传回相应的派生类引用,否则抛出一个bad_cast exception,而不是返回0. 不返回0的原因是,若将一个引用设为0,会使一个临时性对象产生出来,该临时性对象的初值为0,这个引用被设置为这个对象的别名。

class Base{
public:
    virtual void test() {
        cout << "Base class...";
    }
};
 
class Child : public Base{
public:
    void test() {
        cout << "Child class...";
    }
};
 
int main(){
	Base * bptr = new Base;
	Base * bptr_1 = new Child;
	Child * cptr = dynamic_cast<Child *>(bptr); // 错误 因为父类指针并没有指向子类对象 
	Child * cptr_1 = dynamic_cast<Child *>(bptr_1); // 正确 
	cout << cptr << endl << cptr_1;
	return 0;
} 
  • 若对指针进行dynamic_cast,失败返回null,成功返回正常cast后的对象指针;
  • 若对引用进行dynamic_cast,失败抛出一个异常,成功返回正常cast后的对象引用。
  • dynamic_cast在将父类cast到子类时,父类必须要有虚函数,否则编译器会报错, 返回null。
  • dynamic_cast主要用于类层次间的上行转换(将子类指针转为父类指针)下行转换将父类指针转为子类指针),还可以用于类之间的交叉转换。
  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
  • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

dynamic_cas要求所转换的操作数必须包含多态类类型(即至少包含一个虚函数的类)

dynamic_cast转型发生在运行阶段, static_cast转型发生在编译阶段

稳定排序与非稳定排序

稳定排序

冒泡排序(bubble sort — O(n2)

插入排序 (insertion sort— O(n2)

归并排序 ( merge sort — O(n log n)

非稳定排序

选择排序 ( selection sort — O(n2)
希尔排序 ( shell sort — O(n log n)
堆排序 ( heapsort — O(n log n)
快速排序 ( quicksort — O(n log n)

虚函数表创建时机

虚函数表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。

单继承以及多继承下虚函数表的绑定

shared from this 与智能指针的陷阱

首先指出智能指针的一个注意事项:智能指针不能和裸指针混用

shared from this 机制的出现就是为了避免这种情况

通过模板方式继承enable_shared_from_this<T> 然后调用shared_from_this()函数返回对象T的shared_ptr指针,非常方便。使用时需要引用头文件 :#include <memory>

用例:

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>
 
using namespace std;
 
class TestA : public enable_shared_from_this<TestA>
{
public:
    TestA(){ cout << "TestA create" << endl;}

    ~TestA(){ cout << "TesA destory" << endl;}
 
    shared_ptr<TestA> getSharedFromThis() { return shared_from_this(); }
 
};
 
int main(){
 
    {//出了此作用域 ptr1 ptr2 销毁, TestA对象销毁
        shared_ptr<TestA> ptr1(new TestA());
        shared_ptr<TestA> ptr2 = ptr1->getSharedFromThis();
        cout << "ptr1 count: " << ptr1.use_count() << " ptr2 count: " << ptr2.use_count() << endl;
        //输出:ptr1 count: 2 ptr2 count: 2  可以正常释放对象
    }
 
    return 0;
}

问题:为什么要这么麻烦,要继承一个继承enable_shared_from_this<T>模板对象,使用其中的成员函数shared_from_this(),直接使用:

shared_ptr<TestA> getSharedFromThis() { return  shared_ptr<TestA> (this); }

这样不好吗?这样使用就是陷入shared_ptr使用的陷阱里了。我们可以写一个程序试试看:

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>
 
using namespace std;
 
class TestB
{
public:
    TestA(){ cout << "TestA create" << endl;}

    ~TestA(){ cout << "TesA destory" << endl;}
 
    shared_ptr<TestB> getSharedFromThis() { return  shared_ptr<TestB> (this); }
 
};
 
int main(){
     {
        //shared_ptr<TestB> ptr3(new TestB());
        //shared_ptr<TestB> ptr4 = ptr3->getSharedFromThis();
        //cout << "ptr2 count: " << ptr3.use_count() 
               << " ptr4 count: " << ptr4.use_count() << endl;
        //输出:ptr2 count: 1 ptr4 count: 1 然后会崩溃因为重复释放
    }
    cin.get();
    return 0;
}

两个shared_ptr的引用计数都是1,然后释放时,导致对象被释放两遍,导致程序崩溃。

为什么会这样,要从shared_ptr的原理说起;shared_ptr为什么能够在没有shared_ptr指针指向对象时释放对象?

因为所有指向同一个对象的shared_ptr指针共享同一个计数器,当有新的shared_ptr指向对象时,计数器+1,有shared_ptr销毁或者不再指向该对象时,计数器-1,当计数器为0时,对象被销毁。

当如果出现两个shared_ptr指针都指向同一对象,但是计数器不共享时,会导致对象被释放两次,程序出错了,就如同上面的例子。

如何会导致shared_ptr指向同一个对象,但是不共享引用计数器。是因为裸指针与shared_ptr混用,如果我们用一个裸指针初始化或者赋值给shared_ptr指针时,在shared_ptr内部生成一个计数器,当另外一个shared_ptr不用share_ptr赋值或者初始化的话,再次将一个裸指针赋值给另外一个shared_ptr时,又一次生成一个计数器,两个计数器不共享。   

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <memory>
 
using namespace std;
 
int main(){
 
    shared_ptr<int> ptr1(new int(101));
    shared_ptr<int> ptr2(ptr1);
    shared_ptr<int> ptr3;
    ptr3 = ptr1;
 
    shared_ptr<int> ptr4(ptr1.get());
    //ptr1.get()返回ptr1保存对象的裸指针,ptr4会有新的计数器,不跟ptr1,ptr2,ptr3共享计数器
    cout << ptr1.use_count() << " " << ptr2.use_count() << " "
        << ptr3.use_count() << " " << ptr4.use_count() << endl;
    //所以一定不要既用shared_ptr又用对象裸指针(p.get()相当于裸指针)
    cin.get();
    return 0;
}

define(宏定义)与typede区别       

typedef和define都是替一个对象取一个别名,以此增强程序的可读性,

define有一个独特的用法,宏定义在编译前完成替换,相当于直接插入代码,替换之后的代码参与编译,运行时不存在函数调用,运行就更快。函数调用就需要跳转到具体的函数,速度就更慢。

两者区别如下:

原理不同

  • #define是C语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。
  • typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。

功能不同

  • typedef用来定义类型的别名,起到类型易于记忆的功能。
  • #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

作用域不同

  • #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用
  • typedef有自己的作用域。      

对指针的操作不同

#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1, p2; // int * p1, p2;
INTPTR2 p3, p4; // int * p1, * p2;

含义分别为:

  • 声明一个指针变量p1和一个整型变量p2
  • 声明两个指针变量p3、p4

typedef的一些用法和注意点        

1.定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。 

char* pa, pb;     //它只声明了一个指向字符变量的指针和一个字符变量
//以下则可行
typedef char* PCHAR;     //   一般用大写 
PCHAR pa, pb;       //   可行,同时声明了两个指向字符变量的指针 

虽然char   *pa,   *pb也可行,但相对来说没有用typedef的形式直观,尤其在需要大量指针的地方,typedef的方式更省事。 
2.用于struct对象的声明

//原来的写法
struct tagPOINT1 
{ 
    int x; 
    int y; 
}; 
tagPOINT1 p1;   

//修改后的写法
typedef struct tagPOINT 
{ 
    int x; 
    int y; 
}POINT; 
POINT p1; //这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候 

3.定义与平台无关的类型
定义一个叫  REAL  的浮点类型,在目标平台一上,让它表示最高精度的类型为: 

typedef long double REAL;   

在不支持   long   double   的平台二上,改为: 

typedef double REAL;  

在连   double   都不支持的平台三上,改为: 

typedef float REAL;   

也就是说,当跨平台时,只要改下  typedef  本身就行,不用对其他源码做任何修改。标准库就广泛使用了这个技巧,比如size_t。 
4.为复杂的声明定义一个新的简单的别名

//原声明
int *(*a[5])(int, char*); //变量名为a,直接用一个新别名pFun替换a就可以了

typedef int *(*pFun)(int, char*);   
//原声明的最简化版: 
pFun a[5];   

注意点 

typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。比如: 

typedef char* PSTR; 
int mystrcmp(const PSTR, const PSTR); 

const   PSTR实际上不是const   char*,它实际上相当于char*   const。 
原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char*   const。 

缓冲(buffer)与缓存(cache)

Buffer的核心作用是用来缓冲,缓和冲击。

  • 高速设备与低速设备的不匹配,势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区。
  • 比如你每秒要写100次硬盘,对系统冲击很大,浪费了大量时间在忙着处理开始写和结束写这两件事嘛。用个buffer暂存起来,变成每10秒写一次硬盘,对系统的冲击就很小,写入效率高了,极大缓和了冲击。

Cache的核心作用是加快取用的速度。

  • 简单来说,Cache就是用来解决CPU与内存之间速度不匹配的问题,避免内存与辅助内存频繁存取数据,这样就提高了系统的执行效率。
  • 比如一个很复杂的计算做完了,下次还要用结果,就把结果放手边一个好拿的地方存着,下次不用再算了。加快了数据取用的速度。

简单来说就是buffer偏重于写,而cache偏重于读。

sizeof的使用

sizeof()功能:计算数据空间的字节数

  • 与strlen()比较

      strlen()计算字符数组的字符数,以"\0"为结束判断,不计算为'\0'的数组元素,sizeof

会计算'\0'所占的空间
      sizeof计算数据(包括数组、变量、类型、结构体等)所占内存空间,用字节数表示。

char a[10] = "abcde";
cout << sizeof(a) <<endl; //10
cout << strlen(a) <<endl; //5

char src[] = "awedq";
cout << sizeof(src) <<endl; // 6
cout << strlen(src) <<endl; // 5

注意:int *p; sizeof(p)=4;  但sizeof(*p)相当于sizeof(int);      
         对于静态数组,sizeof可直接计算数组大小;
      例:int a[10];   char b[]="hello";
              sizeof(a)等于4*10=40;
              sizeof(b)等于6;
 注意:数组做型参时,数组名称当作指针使用!!
               void  fun(char p[])
               {sizeof(p)等于4}    


经典问题: 
 

      double* (*a)[3][6]; 
      cout<<sizeof(a)<<endl; // 4 a为指针
      cout<<sizeof(*a)<<endl; // 72 *a为一个有3*6个指针元素的数组
      cout<<sizeof(**a)<<endl; // 24 **a为数组一维的6个指针
      cout<<sizeof(***a)<<endl; // 4 ***a为一维的第一个指针
      cout<<sizeof(****a)<<endl; // 8 ****a为一个double变量

union 与struct的空间计算
   总体上遵循两个原则:
   (1)整体空间是 占用空间最大的成员(的类型)所占字节数的整倍数
   (2)数据对齐原则----内存按结构成员的先后顺序排列,当排到该成员变量时,其前面已摆放的空间大小必须是该成员类型大小的整倍数,如果不够则补齐,以此向后类推

struct s1{
    char a;
    double b;
    int c;
    char d;
};

struct s2{
    char a;
    char b;
    int c;
    double d;
};

cout<<sizeof(s1)<<endl; // 24
cout<<sizeof(s2)<<endl; // 16

同样是两个char类型,一个int类型,一个double类型,但是因为对齐问题,导致他们的大小不同。

计算结构体大小可以采用元素摆放法,首先,CPU判断结构体的对界,s1和s2的对界都取长度最大的元素类型,也就是double类型的对界8。然后开始摆放每个元素。

  • 对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
  • 对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。

数组名与指向数组首元素的指针的区别

1.二者均可通过增减偏移量来访问数组中的元素。就是 “[ ]”。

2.数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

3.当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

容器的erase函数

erase()函数用于在顺序型容器中删除容器的一个元素。

有两种函数原型,c.erase(p), c.erase(b,e);

第一个删除迭代器P所指向的元素,第二个删除迭代器b,e所标记的范围内的元素

返回值都是一个迭代器,该迭代器指向被删除元素后面的元素

野指针悬空指针

野指针是指尚未初始化的指针,既不指向合法的内存空间,也没有使用 NULL/nullptr 初始化指针。

悬空指针是指 指针指向的内存空间已被释放或不再有效

构造函数为什么不能为虚函数

从存储空间角度上看,虚函数需要用虚函数表调用,每个对象中存储一个指针,指向虚函数表,执行构造函数时,对象还没实例化,无法通过虚函数表指针寻找虚函数

从使用角度上看,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

什么时候把基类的析构函数必须实现成虚函数?   

基类的指针(引用)指向堆上new出来的派生类对象的时候
它在调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用

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

在C++中,提倡不在构造函数和析构函数中调用虚函数

构造函数和析构函数调用虚函数时都不使用动态绑定,如果在构造函数或析构函数中调用虚函数,则运行的是为基类本类中的版本,而不是子类中的多态版本;

因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态绑定;

析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义,所以析构函数也不支持动态绑定。

构造函数和析构函数抛出异常

  • C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在构造函数中发生异常,析构函数不会被调用。因此会造成内存泄漏。

(解决办法:用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源)

  • 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用 terminate函数让程序结束;

虚函数能为内联函数吗?

多态中虚函数的绑定发生在程序运行期间,而内联函数的展开则发生在编译期间,所以:

  • 当是指向派生类的指针(多态性)调用声明为inline的虚函数时,不会内联展开;
  • 当是对象自己调用本类的虚函数时,会内联展开,当然前提依然是函数并不复杂的情况下

虚函数的绑定以及继承问题

虚函数的静态绑定与动态绑定

静态绑定是编译时期的绑定,绑定的是普通函数的调用

动态绑定是运行时期的绑定,绑定的是对虚函数的调用

动态绑定的条件是父类含有虚函数并且父类的指针Ptr指向派生类对象,再加上子类中对有和父类虚函数返回值,函数签名都一样的函数。那么当使用父类指针Ptr调用虚函数时,会调用到子类中的函数。在此过程中,一直到运行此语句为止,编译器才知道父类函数中的虚函数到底绑定在那个函数体上,这就是动态绑定。

是不是虚函数一定是动态绑定?   

上面的条件中,只要有一个不满足,就不是动态绑定。

  • 在类的构造函数中,调用虚成员函数,是静态绑定。(构造函数中调用其它任何函数,都不会发生动态绑定)。也就是说在有对象之前是不能发生动态绑定的,构造函数执行完才能生成对象;
  • 用父类对象本身调用虚函数,也是静态绑定。

哪些函数不能为虚函数

1) 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数 时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;

2) 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;

3) 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。

4) 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。

5) 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

重载,隐藏,覆盖 

  • 重载:一组函数要重载,必须处在同一作用域下,而且名字相同,参数列表不同(参数个数或类型)
  • 隐藏:在继承结构中,派生类的同名成员,把基类的同名成员给隐藏了,此处不要求返回值和参数一样。比如说子类具有一个和父类成员函数同名的函数,那么用子类对象调用该函数时,调用的是子类中的函数。
  • 覆盖(重写):基类和派生类的方法,返回值、函数名、参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自动处理成虚函数,他们之间成为覆盖关系。

32位与64位下 short int long 长度的区别

 其中,彩色部分代表的意思是该类型在三种平台下所占字节大小不同,黑色说明所占字节数相同!

内存分区 

  • 栈: 存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
  • 堆: 动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
  • 全局区/静态存储区(.bss 段和 .data 段): 存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
  • 常量存储区(.data 段): 存放的是常量,不允许修改,程序运行结束自动释放。
  • 代码区(.text 段): 存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

具体如下: 

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);     
    //分配得来得10和20字节的区域就在堆区。 
    strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
} 

扩展: .data和.bss区以及分区的原因

在c中将数据段分为以上两部分,c++不再区分。

.bss段:Block Started by Symbol,存放未初始化的全局变量。在编译器编译的时候,不会给该段的数据分配空间。BSS节不包含任何数据,只是简单的维护开始和结束的地址,以便内存区能在运行时被有效地清零。

.data段:存放已初始化的全局变量,在编译器编译的时候,会给已初始化的数据分配内存空间,数据保存在目标文件中。

为什么要分.data段和.bss段?

在程序编译的时候,不会给.bss段中的数据分配空间,只是记录数据所需空间的大小。在程序执行的时候,才会给.bss段中的数据分配内存。通过这种方式,可以节省一部分内存空间,进一步缩减可执行程序的大小。

内存泄漏

内存泄漏,是指在程序代码中动态申请的、上的内存 由于某种原因、在使用后没有被释放,进而造成内存的浪费。

产生原因

  • malloc/new申请的内存没有主动释放
  • 使用free释放new申请的内存。 malloc/free以及new/delete必须是各自成对出现,如果混用,就会导致意想不到的情况出现。
  • 使用 delete 而不是 delete[] 释放数组
  • 基类的析构函数没有定义为虚函数

如何避免

  • 谨慎使用动态内存
  • 使用RAII 通过构造函数获取资源,通过析构函数释放资源
  • 使用智能指针

数组sizeof的注意点

即sizeof一个数组的大小时,是按照实际分配到的空间来计算的

#include<bits/stdc++.h>
using namespace std;
int main(){
	int a[][3] = {1,2,3,4,5,6,7};
	cout << sizeof(a[1]) << endl; //12
	cout << sizeof(a) << endl; //36
	return 0;
} 

int *f(int)、int(*f)(int)、int *a[4]、int (*a)[4] 

  • int *f(int) 表示一个名为f的函数,这个函数的功能是返回一个int型指针,参数为int型数据。
  • int(*f)(int) 表示这是一个名为f的函数指针,代表一个参数为int型数据,返回值为int型数据的函数
  • int *a[4] 是一个指针数组,数组名为a,数组中存储着四个int型指针
  • int (*a)[] 指针a指向一个数组的首地址,该数组中存储着int型数据。

堆和栈的区别

堆和栈在不同场景,堆与栈代表不同的含义。一般情况下,有两层含义:

  1. 程序内存布局场景下,堆与栈表示两种内存管理方式(本文介绍的);
  2. 数据结构场景下,堆与栈表示两种常用的数据结构;

一、栈和堆的分配方式不同
栈区(stack):栈的分配方式有两种,动态分配和静态分配,静态分配主要由编译器自动分配释放,存放函数的参数值,局部变量的值等,动态分配由malloc函数进行分配,但分配的地址无需自己释放,操作系统会自行释放。

堆区(heap):堆都是动态分配,一般由程序员分配释放,在C语言中,可以通过free()函数释放,值得注意的是,指向该地址的指针还在,为了误用指针来修改该地址的值,应将赋于空指针。若没有释放,程序结束之后可能由OA回收。

二、栈和堆的存储方式不同

  • 栈是生长方向是向下,内存地址由高到低,即先分配的变量比后分配的变量内存地址高;程序中的局部变量,函数的参数值就放在栈区,不仅如此,函数的调用过程,函数的返回地址,相关参数,寄存器的内容等都存储在栈中,因此,栈的用途比堆广。
  • 堆的生长方向是向上,内存地址由低到高,可以由程序员自行分配,数组等,堆的堆顶一般存放堆的大小,而堆的具体内容可以自己填充。

为什么栈是向下生长,而堆是下生长。

假设栈和堆都是向上生长,那么在程序设计的前期,我们就得指定栈和堆的一个严格的分界线,因为无论是堆还是栈,在内存中使用中都要防止非法的越界,越界将会导致非法内存访问,可能会摧毁整个程序。而这个分界线应该设计在哪里,倘若平均分,但是有的程序使用堆的空间比较多,而有的程序使用栈的空间。当一个栈因为使用空间不足而奔溃的时候,其实还剩下很多堆的空间,而我们又无法使用到这些空间,就会导致这块空间白白浪费。而采用堆向上生长,栈向下生长,就能使空间的使用率得到最大化。

三、堆和栈的效率不同

栈的效率比堆的高,由于堆大量使用malloc()申请空间,容易就造成大量的内存碎片,并且可能引发用户态和核心态的切换,效率较低。栈主要由系统自动分配和处理,效率较高。

四、堆和栈能申请空间的大小不同

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

static关键字

c/c++共有

  • 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。       
  • 修饰局部变量时,表明该变量的值不会因为函数终止而丢失。              
  • 修饰函数时,表明该函数只在同一文件中调用。

c++独有:

  • 修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归 所有对象共有。
  • 用static修饰不访问非静态数据成员的类成员函数。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量

首先明确,静态变量位于内存中的全局数据区,下面展开讨论

静态全局变量定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。

static int i = 1;  
int foo(){
	i += 1;
	return i;
}

使用静态全局变量主要是为了变量的文件隔离, 即静态全局变量不能被其它文件用extern关键字取用(不加static的全局变量可以);其它文件中可以定义相同名字的变量,不会发生冲突。

当然,如果其他文件将本文件用#include“”引用,在其它文件也可以用该全局静态变量,并且各个文件的全局静态变量是互相独立的。

静态局部变量:用于函数体的内部修饰变量,这种变量的生存期长于该函数。如下

int f(){
    static int i = 1;
    i+=1;
    return i;
}

 该变量只会初始化一次,也就是第一次进入函数时,后面进入函数时,静态变量会保持其原来的值,不会重复初始化。但函数结束时,静态变量不会被销毁,只有当程序本身结束时,它才会被释放。

静态局部变量和静态全局变量的生命周期一样,始终驻留在全局数据区,直到程序运行结束。

静态局部变量的作用域为局部作用域,也就是不能在函数体外边使用它(而局部变量在栈区,在函数结束后立即释放内存)

静态函数:与静态全局变量类似,不能被其他文件用extern关键字所调用。

include <stdio.h>
static void fn(){
	printf("this is non-static func in a");
}

类中静态成员变量:用于修饰class的数据成员,这种数据成员的生存期大于class的对象。静态数据成员每个class都只有一份,多个对象共用这一份,普通数据成员是每个class对象都有一份,因此静态数据成员也叫作类变量,而普通数据成员也叫作实例变量

静态数据成员存储在全局数据区,所以它不会增大类的占用空间。

类中静态成员函数:用于修饰class的成员函数。静态成员数据不能访问非静态数据(包括成员函数和成员变量),但是非静态数据可以访问静态数据(也就是成员函数可以访问静态成员变量以及调用静态成员函数)

静态成员函数调用时,既可以用类名加域解析符::,也可以用对象名加'.'

static的一些辨析

在头文件把一个变量申明为static变量,那么引用该头文件的源文件能够访问到该变量吗。

可以。声明static变量一般是为了在本cpp文件中的static变量不能被其他的cpp文件引用,但是对于头文件,因为cpp文件中包含了头文件,故相当于该static变量在本cpp文件中也可以被见到。当多个cpp文件包含该头文件中,这个static变量将在各个cpp文件中将是独立的,彼此修改不会对相互有影响。

为什么静态成员函数不能申明为const

  • const修饰符用于表示函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,而类中的static函数本质上是全局函数,没有this指针,不能用const来修饰它。
  • 一个静态成员函数访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了

静态函数为什么不能为虚函数

  • static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
  • 静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来 访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。对于静态成员函数,它没有 this指针,所以无法访问vptr。

为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义

静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。

static关键字为什么只能出现在类内部的声明语句中,而不能重复出现在类外的定义中。

如果类外定义函数时在函数名前加了static,因为作用域的限制,就只能在当前cpp里用,类本来就是为了给程序里各种地方用的,其他地方使用类是包含类的头文件,而无法包含类的源文件。

为什么常量静态成员数据的初始化可以放在类内 

static数据成员在类外定义和初始化是为了保证只被定义和初始化一次,这样编译器就不必考虑类的函数里面第一个对static变量的’=’操作是赋值还是初始化了。 static const int可以在类里面初始化,是因为它既然是const的,那程序就不会再去试图初始化了。 

为什么静态成员函数只能访问静态成员变量

  • 静态成员函数只属于类本身,随着类的加载而存在,不属于任何对象,是独立存在的
  •  非静态成员当且仅当实例化对象之后才存在,静态成员函数产生在前,非静态成员函数产生在后,故不能访问
  • 内部访问静态成员用self::,而访问非静态成员要用this指针,静态成员函数没有this指针,故不能访问。   

静态成员函数与非静态成员函数的区别

 根本区别:静态成员函数不存在this指针,不能访问非静态成员变量。

volatile关键字

volatile与编译器的执行优化有关,volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

extern关键字

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明此变量/函数是在别处定义的,要在此处引用,用extern会加速程序的编译过程,这样能节省时间

extern是表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用,与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用

在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern C声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。

注意:extern "C"指令中的"C",表示的一种编译和连接规约,而不是一种语言。"C"表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。

还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。

扩展:

  • c++中调用c代码,只须在c++中为c代码函数声明之前加上extern "C"
  • c语言调用c++代码,则需要将c++代码g++编译成静态库或动态库,然后对外提供用extern "C"声明的类c头文件

explicit关键字

C++中的explicit关键字只能用于修饰只有一个参数的类构造函数(或者除了第一个参数以外的其他参数都有默认值),它的作用是表明该构造函数是显示的,用于防止类构造函数的隐式自动转换.。而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).

decltype关键字

decltype是C++11新增的一个关键字,和auto的功能一样,用来在编译时期进行自动类型推导。引入decltype是因为auto并不适用于所有的自动类型推导场景

auto varName=value;
decltype(exp) varName=value;

auto根据等号右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,与等号右边的value没有关系
auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导

原则上将,exp只需要是一个普通的表达式,它可以是任意复杂的形式,但必须保证exp的结果是有类型的,不能是void;如exp为一个返回值为void的函数时,exp的结果也是void类型,此时会导致编译错误

final和override关键字

这两个关键字都是c++11新添加进来的

final 关键字来限制某个类不能被继承,或者某个虚函数不能被重写有一种类或函数到当前版本就终止的意味,不需要再往下延伸了。

如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面

class Base{
public:
    virtual void test() {
        cout << "Base class...";
    }
};
 
class Child : public Base{
public:
    void test() final {
        cout << "Child class...";
    }
};
 
class GrandChild : public Child{
public:
    // 语法错误, 不允许重写
    void test() {
        cout << "GrandChild class...";
    }
};
 
class Child_1 final: public Base{
public:
    void test() {
        cout << "Child class...";
    }
};
 
// error, 语法错误
class GrandChild_1 : public Child{
    public:
};

override 关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,和final一样这个关键字要写到方法的后面。 

class Base{
public:
    virtual void test() {
        cout << "Base class...";
    }
};
 
class Child : public Base{
public:
    void test() override {
        cout << "Child class...";
    }
};
 
class GrandChild : public Child{
public:
    void test(int i) override {// 出错 与父类的虚函数函数签名不一致
        cout << "Child class...";
    }
};

delete 和 default关键字

delete关键字通常用来禁用类中的成员函数,使其无法被调用。

default关键字则用来生成默认的构造函数。

以下代码在调用赋值运算符时,会调用失败

class A {
    public:
        A() = default;
        A(const A&) = delete;
};

int main(int argc, char const *argv[])
{
    A a;
    A a2 = a;
    return 0;
}
无法引用 函数 "A::A(const A &)" (已声明 所在行数:49) -- 它是已删除的函数C/C++(1776)

构造函数为什么不能为const属性

当我们创建一个类的const对象时,直到构造函数完成初始化的过程,对象才真正取得其“常量”的属性, 所以从这个角度讲,构造函数没必要声明为const函数,因为构造函数在进行const对象的成员初始化工作的时候,在构造函数看来该对象还仍然是非const的属性,在执行构造函数期间还是可以改变对象的数据,所以构造函数也就没必要声明为const属性。

引用类型和const类型的数据为什么不能在构造函数内部初始化

因为在进入构造函数体内时,引用变量和const变量都已经用不确定的值初始化好了,构造函数内能做的只有赋值,而const类型和引用类型是不可以赋值的。所以,需要在初始化列表中初始化。

类内不同数据的初始化方式

  • 静态常量数据成员(staic const)可以在类内初始化(即类内声明的同时初始化),也可以在类外,即类的实现文件中初始化,不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化;
  • 静态非常量数据成员(static)只能在类外,即类的实现文件中初始化,也不能在构造函数中初始化,不能在构造函数的初始化列表中初始化;
  • 非静态的常量数据成员(const)不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化
  • 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化;

必须要用初始化列表初始化的情况

在c++中,赋值操作会产生临时对象,所以说只能初始化列表来进行初始化的情况,其实就是一些数据不能够重复赋值,在初始化的时候就要进行赋值。

比如类内的const成员和引用成员,它们在初始化结束之后就不能再赋其他值,如果在构造函数里进行初始化的话,会先建立一个临时对象用随机值初始化,之后再进行赋值,这就发生了两次赋值。

还有必须要用初始化列表的时,类里面有其他的类对象成员或者继承而来的类,而这个成员的类和父类没有默认构造函数,只有带参构造函数,此时也只能用初始化列表,因为编译器无法调用它们的默认构造函数来建立临时对象。

一个派生类构造函数的执行顺序

① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③ 类类型的成员对象的构造函数(按照初始化顺序)

④ 派生类自己的构造函数。

new和malloc的区别

  • new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;
  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而 malloc则需要显式地指出所需内存的尺寸
  • new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
  • new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的 构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存, 无法强制要求其做自定义类型对象构造和析构工作

malloc的底层实现(brk与mmap)

先说结论

  • 当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  • 当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

内存分配的原理

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

  • brk是将数据段(.data)的最高地址指针_edata往高地址推;
  • mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存

这两种方式分配的都是虚拟内存,没有分配物理内存在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

下面以一个例子来说明内存分配的原理:

情况一 :malloc小于128k的内存,使用brk分配内存

进程启动的时候,其(虚拟)内存空间的初始布局如图1所示。其中,mmap内存映射文件是在堆和栈的中间,_edata指针(glibc里面定义)指向数据段的最高地址。
进程调用A=malloc(30K)以后,malloc函数会调用brk系统调用,将_edata指针往高地址推30K,就完成虚拟内存分配。

需要注意的是,_edata+30K只是完成虚拟地址的分配,A这块内存现在还是没有物理页与之对应的,等到进程第一次读写A这块内存的时候,发生缺页中断,这个时候,内核才分配A这块内存对应的物理页,并将虚拟地址与物理地址建立映射关系。也就是说,如果用malloc分配了A这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的

情况二:malloc大于128k的内存,使用mmap分配内存

进程调用C=malloc(200K),默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。

这样子做主要是因为 brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因),而mmap分配的内存可以单独释放
 

进程调用free(B)以后,B对应的虚拟内存和物理内存都没有释放,因为只有一个_edata指针,当然B这块内存是可以重用的,如果这个时候再来一个40K的请求,那么malloc很可能就把B这块内存返回回去了。
进程调用free(D)以后,B和D连接起来,变成一块140K的空闲内存。

默认情况下,当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

C 和 C++ 区别

首先,C 和 C++ 在基本语句上没有过大的区别。

C++ 有新增的语法和关键字,语法的区别有头文件的不同和命名空间的不同,C++ 允许我们自己定义自己的空间,C 中不可以。

关键字方面比如 C++ 与 C 动态管理内存的方式不同,C++ 中在 malloc 和 free 的基础上增加了 new 和 delete,而且 C++ 中在指针的基础上增加了引用的概念, C++中还增加了 auto,explicit 体现显示和隐式转换上的概念要求,还有 dynamic_cast 增加类型安全方面的内容。

函数方面 C++ 中有重载和虚函数的概念:C++ 支持函数重载而 C 不支持,是因为 C++ 函数的名字修饰与 C 不同,C++ 函数名字的修饰会将参数加在后面,例如,int func(int,double)经过名字修饰之后会变成funcint_double,而 C 中则会变成 _func,所以 C++ 中会支持不同参数调用不同函数

类方面,C 的 struct 和 C++ 的类也有很大不同:C++ 中的 struct 不仅可以有成员变量还可以成员函数,而且对于 struct 增加了权限访问的概念,struct 的默认成员访问权限和默认继承权限都是 public,C++ 中除了 struct 还有 class 表示类,struct 和 class 还有一点不同在于 class 的默认成员访问权限和默认继承权限都是 private。

C++ 中增加了模板还重用代码,提供了更加强大的 STL 标准库。

最后补充一点就是 C 是一种结构化的语言,重点在于算法和数据结构。C 程序的设计首先考虑的是如何通过一个代码,一个过程对输入进行运算处理输出。而 C++ 首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题领域,这样就能通过获取对象的状态信息得到输出。

C 的 struct 更适合看成是一个数据结构的实现体,而 C++ 的 class 更适合看成是一个对象的实现体。

C++ 和 Java 区别

指针:Java 语言让程序员没法找到指针来直接访问内存,没有指针的概念,并有内存的自动管理功能,从而有效的防止了 C++ 语言中的指针操作失误的影响。但并非 Java 中没有指针,Java 虚拟机内部中还是用了指针,保证了 Java 程序的安全。

多重继承:C++ 支持多重继承但 Java 不支持,但支持一个类继承多个接口,实现 C++ 中多重继承的功能,又避免了 C++ 的多重继承带来的不便。

数据类型和类:Java 是完全面向对象的语言,所有的函数和变量必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可以实现自己的特点和行为。Java 中取消了 C++ 中的 struct 和 union 。

自动内存管理:Java 程序中所有对象都是用 new 操作符建立在内存堆栈上,Java 自动进行无用内存回收操作,不需要程序员进行手动删除。而 C++ 中必须由程序员释放内存资源,增加了程序设计者的负担。Java 中当一个对象不再被用到时, 无用内存回收器将给他们加上标签。Java 里无用内存回收程序是以线程方式在后台运行的,利用空闲时间工作来删除。

Java 不支持操作符重载。操作符重载被认为是 C++ 的突出特性。

Java 不支持预处理功能。C++ 在编译过程中都有一个预编译阶段,Java 没有预处理器,但它提供了 import 与 C++ 预处理器具有类似功能。

类型转换:C++ 中有数据类型隐含转换的机制,Java 中需要限时强制类型转换。

字符串:C++中字符串是以 Null 终止符代表字符串的结束,而 Java 的字符串 是用类对象(string 和 stringBuffer)来实现的。

Java 中不提供 goto 语句,虽然指定 goto 作为关键字,但不支持它的使用,使程序简洁易读。

Java 的异常机制用于捕获例外事件,增强系统容错能力。

TCP与UDP区别

父进程与子进程之间的关系

HTTP三次握手并且说明双方的序号

左值引用与右值引用

在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

例如 int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

  • 左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
  • 右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存 也可以在CPU寄存器。

一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。

左值引用
左值引用就是我们平常使用的“引用”。引用是为对象起的别名,必须被初始化,与变量绑定到一起,且将一直绑定在一起。我们通过 & 来获得左值引用,
type & 引用名 = 左值表达式;
可以把引用绑定到一个左值上,而不能绑定到要求转换的表达式、字面常量或是返回右值的表达式。举个例子:

int i = 42;
int &r = i;    //正确,左值引用
int &r1 = i * 42;   //错误, i*42是一个右值
const int &r2 = i * 42; //正确,可以将一个const的引用绑定到一个右值上

右值引用
右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递,主要解决移动语义和完美转发,右值引用可以指向右值,但是无法指向左值。

它的主要目的有两个方面:

  • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  • 能够更简洁明确地定义泛型函数。

右值引用就是必须绑定到右值的引用,他有着与左值引用完全相反的绑定特性,我们通过 && 来获得右值引用。

右值引用的基本语法type &&引用名 = 右值表达式;

右值有一个重要的性质——只能绑定到一个将要销毁的对象上。如下

int  &&rr = i;  //错误,i是一个变量,变量都是左值
int &&rr1 = i *42;  //正确,i*42是一个右值

右值引用和左值引用的区别
左值可以寻址,而右值不可以。
左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。   

段错误

段错误是计算机软件运行过程中可能出现的一种特殊错误情况。当程序试图访问不允许访问的内存位置,或试图以不允许的方式访问内存位置(例如尝试写入只读位置,或覆盖部分操作系统)时会发生段错误。

分段是操作系统内存管理和保护的一种方法。在大多数情况下,它已经被分页所取代,但是分段的许多术语仍然被使用,“分段错误”就是一个例子。尽管分页被用作主内存管理策略,但有些操作系统在某些逻辑级别上仍然有分段。在类Unix操作系统上,访问无效内存的进程接收SIGSEGV信号。在Microsoft Windows上,访问无效内存的进程会收到状态“访问冲突”异常。

段错误发生的原因       

  • 使用非法的内存地址(指针),包括使用未经初始化及已经释放的指针、不存在的地址、受系统保护的地址,只读的地址等
  • 内存读/写越界,包括数组访问越界,或在使用一些写内存的函数时,长度指定不正确或者这些函数本身不能指定长度,典型的函数有strcpy(strncpy),sprintf(snprint)等等。
  • 对于C++对象,应该通过相应类的接口来去内存进行操作禁止通过其返回的指针对内存进行写操作,典型的如string类的c_str()接口,如果你强制往其返回的指针进行写操作肯定会段错误的,因为其返回的地址是只读的。
  • 函数返回其中局部对象的引用或地址,当函数返回时,函数栈弹出,局部对象的地址将失效,改写或读这些地址都会造成未知的后果。
  • 在栈中定义过大的数组,可能导致进程的栈空间不足,此时也会出现段错误,同样的,在创建进程/线程时如果不知道此线程/进程最大需要多少栈空间时最好不要在代码中指定栈大小,应该使用系统默认的。
  • 操作系统的相关限制,如:进程可以分配的最大内存,进程可以打开的最大文件描述符个数等,在Linux下这些需要通过ulimit、setrlimit、sysctl等来解除相关的限制,这类段错误问题在系统移植中经常发现
  • 跨进程传递某个地址,传递的都是经过映射的虚拟地址,对另外一个进程是不通用的。

内联函数与宏定义的区别

内联(inline)函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接嵌入到目标代码中。

内联函数适用场景

1.使用宏定义的地方都可以使用inline函数

2.作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率

为什么不能把所有的函数写成内联函数

内联函数以代码的膨胀为代价,它以省去函数调用的开销来提高执行效率。所以一方面如果内联函数体内代码执行时间相比原来函数调用执行时间长,则没有太大的意义;另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:

1.函数体内的代码比较长,将导致内存消耗代价

2.函数体内有循环,函数执行时间要比函数调用开销大

主要区别

  • 内联函数在编译时展开,宏在预编译时展开
  • 内联函数直接嵌入到目标代码中,宏是简单的做文本替换
  • 内联函数有类型检测、语法判断等功能,而宏没有
  • 内联函数本身是函数,强调函数特性,具有重载等功能,宏不是
  • 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义
  • 内联函数代码是被放到符号表中,使用时像宏一样展开,没有调用的开销,效率很高
  • 在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
  • 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员,进而提升效率。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了。

NULL与nullptr

NULL来自C语言,一般由宏定义实现,而 nullptr 则是C++11的新增关键字。

在C语言中,NULL被定义 为(void*)0, 而在C++语言中,NULL则被定义为整数0。

关于this指针

this指针是类的指针,指向对象的首地址。但一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部, 当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针), 编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针, 编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this 进行

this指针的使用

  • 在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;
  • 当形参数与成员变量名相同时用于区分,如this->n = n (不能写成n = n);

this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个 参数为T * const this

class A{
public:
    int func(int p){}
};
其中,func的原型在编译器看来应该是:
int func(A * const this,int p);

this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this 参数传递进去。如: 

A a;
a.func(10);
//此处,编译器将会编译成:
A::func(&a,10);

静态绑定与动态绑定

1.静态类型:对象在声明时采用的类型,在编译期确定;

2.动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

3.静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期

4.动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期

非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性),对象的动态类型可以更改,但是静态类型无法更改

如下,一个类的对象在初始化之后它的静态类型就再也不会变化,不管它指向什么子类,最终执行的函数任然是初始化时绑定的类型。

#include <iostream>
using namespace std;
class A
{
public:
    void func() { std::cout << "A::func()\n"; }
};
class B : public A
{
public:
    void func() { std::cout << "B::func()\n"; }
};
class C : public A
{
public:
    void func() { std::cout << "C::func()\n"; }
};
int main()
{
    C* pc = new C();
    B* pb = new B(); 
    A* pa = pc;      
    pa = pb;         
    C* pnull = NULL;

    pa->func();      //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类
    pc->func();      //C::func() pc的静态类型都是C*,因此调用C::func();
    pnull->func();   //C::func() 不用奇怪为什么空指针也可以调用函数,
                     //因为这在编译期就确定了,和指针空不空没关系;
    return 0;
}

怎样判断两个浮点数的大小

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!

对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较 也应该注意。与浮点数的表示方式有关。

字节内存对齐

条件1.分配内存的顺序是按照声明的顺序。

条件2.结构体变量的首地址能够被字节对齐的大小整除(gcc 缺省字节对齐大小是4)。

条件3.结构体的每个成员相对首地址的偏移是成员类型大小的整数倍(成员自身对齐)

条件4.结构体变量的总大小是结构体里最大的成员的整数倍


首先可以用下面的语句来取消内存对齐

__attribute__ ((packed)); //取消结构在编译过程中的优化对齐。

具体的,说应该是取消条件4,也就是结构体总的大小不需要是内部最大的成员长度的整数倍。但是条件3仍需要满足,如下,int型数据a的的距离结构体起始地址的偏移地址必须是4字节的整数倍,所以char型数组d所占的空间为4字节,以便a的偏移地址是4的倍数。

但是因为不需要满足总的大小是8字节(有double)的倍数,所以char b字符只占用一个字节空间,不需要再进行扩充,最后结构体总的大小为17;而如果将__attribute((packed))这一句去掉,结构体的大小就变成了24(因为总的大小要是8字节的倍数)

struct tst5{
	char d[3];//3(4)
	int a;//4
	double c;//8	
	char b;//1	
}__attribute((packed));

sizeof(tst5) = 17

接着是修改条件4的语句,如下,让所作用的结构体、类的成员对齐在n字节自然边界上 。如果结构中有成员的长度大于n,则按照机器字长来对齐。n=1,2,4,8,16…

__attribute__ ((aligned (n))); 
  • 当n大于结构体内最长数据的长度len时,结构体内部各部分数据偏移量还是按len计算,但是结构体总的长度要是n的倍数。
  • 当当n小于结构体内最长数据的长度len时,结构体内部各部分数据偏移量和结构体总的长度都要是len的倍数。
  • 特殊的,当n < len,并且len不是2的幂指数时,结构体内部各部分数据偏移量和结构体总的长度都要是n的倍数。
struct tst2{
	char a[3];//3(4)
	int c;//4	
	char d;//1(4)
	int b;//4
	int e;//4(8)
}__attribute((aligned(8)));

sizeof(tst2) = 24

struct tst3{
	char a;//1(8)
	double  c;//8
	char b[3];//3(8)
}__attribute((aligned(4)));

sizeof(tst3) = 24

struct tst4{
	char a;//1(4)
	int  c;//4
	char b[9];//9(12)
}__attribute((aligned(4)));

sizeof(tst4) = 20

为什么模板类一般都是放在一个H文件中

1) 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

2) 在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来。

所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来

然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj 中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。

cout与printf区别

cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以 会自动识别数据的类型。输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。

cout是有缓冲输出

flush立即强迫缓冲输出。

printf是无缓冲输出。有输出时立即输出

delete this 的后果

在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放 在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它。 当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就 会出现不可预期的问题。

为什么是不可预期的问题?

delete this之后理应释放了类对象的内存空间,这段内存应该已经还给系统,不再属于这个进程。照这个逻辑来看,应该发生指针错误,无访问权限之类的令系统崩溃的问题。但实际上并非如此,这个问题牵涉到操作系统的内存管理策略。delete this释放了类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他什么原因,导致这段内存空间暂时并没有被系统收回。此时这段内存是暂时可以访问的,可以加上100,200的地址偏移量,但是其中的值却是不确定的。当获取数据成员,可能得到的是一串很长的未初始化的随机数;访问虚函数表,指针无效的可能性非常高,造成系统崩溃。

如果在类的析构函数中调用delete this会发生什么情况

会导致堆栈溢出。delete的本质是“为将被释放的内存调用一个或多个析构函数,然后释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。

什么时候需要合成构造函数

  1. 如果一个类没有任何构造函数,但他含有一个成员对象,该成员对象含有默认构造函数,那么编译 器就为该类合成一个默认构造函数,因为不合成一个默认构造函数那么该成员对象的构造函数不能调用;
  2. 没有任何构造函数的类派生自一个带有默认构造函数的基类,那么需要为该派生类合成一个构造函 数,只有这样基类的构造函数才能被调用;
  3. 带有虚函数的类,虚函数的引入需要进入虚表,指向虚表的指针,该指针是在构造函数中初始化 的,所以没有构造函数的话该指针无法被初始化;
  4. 带有一个虚基类的类

还有一点需要注意的是:

1) 并不是任何没有构造函数的类都会合成一个构造函数

2) 编译器合成出来的构造函数并不会显示设定类内的每一个成员变量

什么时候需要合成拷贝构造函数

有三种情况会以一个对象的内容作为另一个对象的初值调用拷贝构造函数:

1) 对一个对象做显示的初始化操作,X xx = x;

2) 当对象被当做参数交给某个函数时;

3) 当函数传回一个类对象时;

需要编译器合成的情况:

1) 如果一个类没有拷贝构造函数,但是含有一个类类型的成员变量,该类型含有拷贝构造函数,此时 编译器会为该类合成一个拷贝构造函数;

2) 如果一个类没有拷贝构造函数,但是该类继承自含有拷贝构造函数的基类,此时编译器会为该类合成一个拷贝构造函数;

3) 如果一个类没有拷贝构造函数,但是该类声明或继承了虚函数,此时编译器会为该类合成一个拷贝构造函数;

4) 如果一个类没有拷贝构造函数,但是该类含有虚基类,此时编译器会为该类合成一个拷贝构造函数;

strcpy和memcpy的区别

1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。

2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。

3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

数组指针和指针数组 的区别

数组指针(也称行指针)
定义:int (*p)[n];
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。

如要将二维数组赋给一指针,应这样赋值:

int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a;         //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++;         //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

关于二维数组指针的一些计算

int a[3][4] = {1, 2, 3, 4,
			   5, 6, 7, 8,
			   0, 0, 0, 0};
int (*p) [3][4]	;
cout << sizeof(a) << " " << sizeof(p) << endl;// 48; 4
cout << sizeof(*a) << " " << sizeof( *(*a) ) <<" " << sizeof(&a) << endl;// 16; 4; 4
cout << a << " " << a + 1 << endl;//指向16个字节之后 
cout << p << " " << p + 1 << endl;//指向48个字节之后 
cout << *(*(a + 1) + 1) << endl;//相当于输出a[1][1] 


//输出:
48 4
16 4 4
0x6cfe5c 0x6cfe6c
0x77c94ea2 0x77c94ed2
6

指针数组
定义:int *p[n];
[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素。

const char * 与 string 之间的转换

string 是c++标准库里面其中一个,封装了对字符串的操作,实际操作过程我们可以用const char*给 string类初始化

//a) string转const char*
string s = “abc”;
const char* c_s = s.c_str();

//b) const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);

//c) string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());

//d) char* 转string
char* c = “abc”;
string s(c);

//e) const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);

//f) char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;

拷贝构造函数为什么只能用引用来传递参数而不能用传值

拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实 例。

值传递时,对于内置数据类型的传递时,直接赋值拷贝给形参; 对于用户自定义的类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参

引用传递时,无论对内置类型还是类类型,传递引用最终都是传递的是实参的间接地址值,而不会有拷贝构造函数的调用

拷贝构造函数使用值传递会产生无限递归调用,内存溢出。

拷贝构造函数如果用传值的方式进行传参数,那么实参需要调用拷贝构造函数来初始化形参,而形参调用的拷贝构造函数需要传递实参,所以会一直递归。

关于回调函数

  • 回调函数就相当于一个中断处理函数,由系统在符合用户设定的条件时自动调用。为此需要做三 件事:1,声明;2,定义;3,设置触发条件。
  • 回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数;
  • 因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个 具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

程序从代码到可执行文件所经历的过程

预编译

主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:

1. 删除所有的#define,展开所有的宏定义。

2. 处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

3. 处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。

4. 删除所有的注释,“//”和“/**/”。

5. 保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。

6. 添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。

编译

把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。

1. 词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。

2. 语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的 语法树是一种以表达式为节点的树。

3. 语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。

4. 优化:源代码级别的一个优化过程。

5. 目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。

6. 目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。

汇编

将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程由汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一 样)xxx.o(Windows 下)、xxx.obj(Linux下)。

链接

将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:

静态链接

函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。

缺点:

  • 空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
  • 更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。

优点:

运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

动态链接

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

优点:

共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;

更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。

缺点:

性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失


容器内部删除一个迭代器

  • 顺序容器(序列式容器,比如vector、deque)

        erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器

  • 关联容器(关联式容器,比如map、set、multimap、multiset等)

        erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代 器;


vector释放空间

由于vector的内存占用空间只增不减,比如首先分配了10,000个字节,然后erase掉后面9,999个,留下 一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。 empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。

如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助释放内存。

vector().swap(Vec); //清空Vec的内存;

vector的reserve()和resize()方法之间有什么区别

  • 首先, vector的容量capacity( )是指在不分配更多内存的情况下可以保存的最多元素个数,而vector的大小size( )是指实际包含的元素个数;
  • vector的reserve(n)方法只改变vector的容量,如果当前容量小于n ,则重新分配内存空间,调整容量为n ;如果当前容量大于等于n,则无操作;
  •  vector的resize(n)方法改变vector的大小,如果当前容量小于n,则调整容量为n,同时将其全部元素填充为初始值;如果当前容量大于等于n,则不调整容量,只将其前n个元素填充为初始值。

map中[]与find的区别

  • map的下标运算符[]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map
  • map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器

哈希表中解决冲突的方法

重点记住前三个:

  • 线性探测:使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位
  • 开链:每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序存在这个list中
  • 再散列:发生冲突时使用另一种hash函数再计算一个地址,直到不冲突
  • 二次探测:使用hash函数计算出的位置如果已经有元素占用了,按照$1^2$、$2^2$、$3^2$...的步长依次寻找,如 果步长是随机数序列,则称之为伪随机探测
  • 公共溢出区:一旦hash函数计算的结果相同,就放入公共溢出区

RTTI       

RTTI (Run Time Type Identification) 即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。 

由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。

C++通过以下两个关键字提供RTTI功能:

  1. typeid:该运算符返回其表达式或类型名的实际类型
  2. dynamic_cast:该运算符将基类的指针或引用安全地转换为派生类类型的指针或引用(也就是所谓的下行转换)

指针问题 

首先有一个需要注意的是,如下

int a = 1;
int * p = &a;
p = p + 1;

假设此时指针p存储的地址值是0x12ff60,这个值代表了int型数据在内存中的首地址,实际上int型数据在内存中的范围是0x12ff60~0x12ff63,(地址以字节为单位进行个数计算)。如果执行

p = p + 1,那么得到的值应该是p的值加上int型数据在内存中的长度,也就是0x12ff64。

进一步的,如下

int a[5] = {1,2,3,4,5};
int * p = (int *)(&a + 1);
cout << *(a + 1) << *(p - 1) << endl;
//输出 2 5

a表示的是int型数组的首个元素的地址,那么 a + 1也就是数组的第二个元素。

&a表示的是整个数组在内存中的地址,并且因为这个数组的大小有5个字节,实际上&a + 1表示的是,除去这五个字节的空间,系统分配的地址,所以是数组最后一个元素所占内存空间的下一个字节空间

function类模板

function类模版,通常用在函数的形参列表或者自定义对象的接收参数类型里,如下:

typedef std::function<void()> CallBack;
void setReadHandler(CallBack&& readHandler) { 
    readHandler_ = readHandler; 
}

map<string, function<int(int, int)>> binops;

我们希望把几个可调用对象看作一种类型,共享同一种调用形式,如下:

// 普通函数
int add(int i, int j) { return i + j; }

// lambda, 其产生一个未命名的函数对象类
auto mod = [](int i, int j) { return i % j; };

// 函数对象类
struct divide {
	int operator() (int denominator, int divisor) {
		return denorninator / divisor;
	};
};

这三种可调用函数,但是想要将它们作为参数传入下面的map数据结构中,往往是不可行的,比如每个 lambda 有它自己的类类型, 该类型与存储在 binops 中的值的类型不匹配

map<string, int(*) (int, int)> binops;

所以需要这样一个类模板,能够接收各种各样的形如 int (int, int)这样的函数,并把它们作为一类,这就是function类模板的作用。使用方法如下:

map<string, function<int(int, int)>> binops = {
	{"+", add}, 				                      // 函数指针
	{ "-", std::minus<int>()},	                 // 标准库函数对象
	{"I", divide()},			             // 用户定义的函数对象
	{"*", [](int i, int j) { return i * j; }},	// 未命名的lambda
	{"%", mod}};				           // 命名了的 lambda 对象
};

binops["+"](10, 5); //调用 add(10, 5)
binops["-"](10, 5); //使用 minus<int> 对象的调用运算符
binops["/"](10, 5); //使用 divide 对象的调用运算符
binops["*"](10, 5); //调用 lambda 函数对象
binops["%"](10, 5); //调用 lambda 函数对象

bind函数

auto newCallable = bind(callable, arg_list);

bind函数看做一个通用的函数适配器,主要用于参数的绑定和设置,它可以把原可调用对象callable的某些参数预先绑定到给定的变量中(也叫参数绑定),然后产生一个新的可调用对象newCallable。

在c语言中, 回调函数的实现往往通过函数指针来实现。 但是在c++中 , 如果回调函数是一个类的成员函数。这时想把成员函数设置给一个回调函数指针往往是不行的,因为类的成员函数,多了一个隐含的参数this。 所以直接赋值给函数指针肯定会引起编译报错。

一种过去常用的方法就是把该成员函数设计为静态成员函数(因为类的成员函数需要隐含的this指针 而回调函数没有办法提供),但这样做有一个缺点,就是会破坏类的结构性,因为静态成员函数只能访问该类的静态成员变量和静态成员函数,不能访问非静态的,要解决这个问题,可以把对象实例的指针或引用做为参数传给它。后面就可以靠这个对象实例的指针或引用访问非静态成员函数。

另一种办法就是使用std::bind和std::function结合实现回调技术。在网络编程中,通过bind函数对类中成员函数进行绑定,生成一个新的可调用对象,并用function类模板来接收它,实现回调函数额绑定。

*p++,*++p,(*p)++

解引用运算符和自增运算符优先级是一样的,从右到左结合来解析

*P++ = *(p++)    *++p = *(++p)

结构体与类区别

类中,对于未指定访问控制属性的成员,其访问控制属性为私有类型

结构体中,对于未指定任何访问控制属性的成员,其访问控制属性为公有类型

C语言中结构体不允许定义函数成员,且没有访问控制属性的概念。

C++为C语言中的结构体引入了成员函数、访问控制权限、继承、包含多态等面向对象特性。

指针和引用的区别

  • 指针保存的是所指对象的地址(实体),引用仅仅是对象的别名(非实体),指针需要通过解引用间接访问,而引用是直接访问;

  • 非空区别:引用不能为空,指针可以为空;

  • 可修改区别:引用必须在定义时就初始化并且不能改变所指的对象,而指针可以改变地址,从而改变所指的对象;

  • 合法性区别:引用是类型安全的,而指针不是 (引用比指针多了类型检查);

  • 指针存在多级指针,但是引用只有一集,不存在引用的引用

vector与list的区别

vector
vector的底层实现是数组,它拥有一段连续的内存空间,并且起始地址不变,因此存取的效率较高。由于他的内存空间是连续的,所以在中间进行插入和删除的操作时,会造成内存块的拷贝,在空间不够的情况下,需要申请一块最够大的内存并进行内存的拷贝,也是基于这也原因,在vetcor的插入和删除是会造成迭代器失效

vector的迭代器失效问题

在插入元素的时候,若是内存空间不够,需要进行扩容,而增容后的pos还是指向原来的空间,但是原来的空间已经释放,这就会导致迭代器失效;(解决办法: 在插入前计算出相对的起始位置的相对距离,那么当插入时候用计算的距离还原pos进行插入)
在删除元素的时候,假设删除pos处的元素,然后再访问pos处的元素,此时是无法访问的,这就会导致迭代器失效。(解决办法: 使用返回值,返回删除后原来的位置。用到的时候,当删除后接收返回的迭代器。)

list
list的底层实现是双向链表,所以他的内存空间是不连续的,因此list的存取的效率比较低,但是基于链表的特性,list的插入和删除的效率非常高。

list的迭代器失效问题

list的插入不会导致迭代器失效,但是删除节点会导致迭代器失效,假设删除pos出的元素,迭代器++就会失效,因为需要通过当前结点来找到下一个结点的,当前结点已经释放,通过迭代器++就会失效(解决办法: 在实现erase的时候会返回删除后的后面的元素的迭代器。)

vector与list的区别

(1)vector 的底层实现是数组,而list的底层实现是双向链表
(2)vector支持随机存取,而list不支持随机存取
(3)vector是顺序内存,而list不是
(4)vector在中间进行插入删除元素时,会导致内存拷贝,而list不会
(5)vector是一次分配好内存,不够时再进行2倍扩容,而list每次插入新结点都会进行内存申请
(6)vector的随机访问性能好,但是插入删除的性能不好,而list 不支持随机访问,但是插入和删除元素的性能好

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
CSDN嵌入式笔试面试题目系列是CSDN提供的面向嵌入式系统工程师的一系列笔试题目,旨在评估面试者在嵌入式领域的知识和技能。 这些面试题目涵盖了嵌入式系统的各个方面,包括硬件设计、嵌入式软件开发、嵌入式操作系统等。通过回答这些题目面试者可以展示他们的专业知识、问题解决能力和团队协作能力。 作为一个嵌入式系统工程师,我会通过以下几个方面来回答这一系列的面试题目: 1. 硬件设计:我会解释如何设计一个嵌入式系统的硬件架构,包括选择核心处理器、外设接口的设计和电路设计等。 2. 嵌入式软件开发:我会谈谈自己的嵌入式软件开发经验,包括使用哪些开发工具和编程语言,如何进行软件调试和优化。 3. 嵌入式操作系统:我会介绍我在嵌入式操作系统方面的经验和知识,包括熟悉的操作系统类型,如RTOS和Linux,以及如何进行任务调度和内存管理等。 4. 项目经验:我会分享我在嵌入式项目中的经验,包括完成的项目类型、任务分工和团队合作等。 5. 学习与发展:我会表达自己对嵌入式领域的学习态度和发展意愿,包括对新技术的关注和学习计划等。 综上所述,CSDN嵌入式笔试面试题目系列是一系列用来评估嵌入式系统工程师技能的笔试题目。通过回答这些题目面试者可以展示他们的专业知识和技能,并展示自己在嵌入式领域的学习态度和发展潜力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值