嵌入式面经汇总(一)

1.关于print函数

printf()

printf("<格式化字符串>", <参量表>);

格式化输出函数

fprintf()

fprintf(fp, "%d", i); /*向所建文件写一整型数*/

fprintf(fp, "%s", s); /*向所建文件写一字符串*/

格式化文件写入

sprintf()

sprintf()的作用是将一个格式化的字符串输出到一个目的字符串中。

sprintf的第一个参数应该是目的字符串,如果不指定这个参数,执行过程中出现     "该程序产生非法操作,即将被关闭...."的提示。
因为C语言在进行字符串操作时不检查字符串的空间是否够大,所以可能会出现数组越界而导致程序崩溃的问题。即使碰巧,程序没有出错,也不要这么用,因为早晚会出错。所以一定要在调用sprintf之前分配足够大的空间给buf。

Snprintf()

Snprintf()函数与Sprintf()函数极为相似,但是该函数多了size参数来表示最大的字符数目,该函数返回一个整数值表示被存储的字符的数目,如果返回-1则表示输出的字符空间不够。

asprintf()函数

int asprintf (char **ptr, const char *template, ...)

本函数跟sprintf()函数很类似,只是它将字符串的分配改成动态分配的形式,参数ptr是指一个char *对象的的指针。(不用自己分配空间)

vprintf()函数

int vprintf (const char *template, va_list ap)

本函数跟printf()函数很类似,只是将参数的数目可变的,变成了一个指针的列表(使用va_start (args, format);来初始化参数列表)

vfprintf()函数

int vfprintf (FILE *stream, const char *template, va_list ap)

本函数跟fprintf函数很类似,只是将参数的数目可变的,变成了一个指针的列表。

vsprintf()函数

int vsprintf (char *s, const char *template, va_list ap)

本函数跟sprintf函数很类似,只是将参数的数目可变的,变成了一个指针的列表。

vsnprintf()函数

int vsnprintf (char *s, size_t size, const char *template, va_list ap)

本函数跟snprintf函数很类似,只是将参数的数目可变的,变成了一个指针的列表。

vasprintf()函数

int vasprintf (char **ptr, const char *template, va_list ap)

本函数跟asprintf函数很类似,只是将参数的数目可变的,变成了一个指针的列表。

2.关于函数调用的过程

参考https://blog.csdn.net/xy294636185/article/details/79999311

自己写一遍理一下思路,看还是看连接里面的吧,有图好理解

在执行调用函数的过程中,计算机通常还要根据函数完成一些工作,这些操作通过形成一个栈帧来完成。栈帧是编译器用来实现函数调用过程的一种数据结构。每个栈帧对应着一个未运行完的函数。

调用main()函数,开辟帧栈空间。

栈帧的需要ebp(基址指针)和esp(堆栈指针)两个寄存器。 在函数调用的过程中这两个寄存器存放了维护这个栈的栈底和栈顶指针 
注意:ebp指向当前位于系统栈最上边(高位)一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。

1.压栈,把ebp放入栈顶,而esp始终指向栈顶 
2.将esp值传给ebp,也就是让esp,ebp移在一起 
3.sub为减的意思,即将esp-0E4h赋给esp,且函数调用分配由高地址向低地址增长,因此esp向上移动,即开辟了新空间,也就是为main函数开辟空间 
4.三个push压栈分别将ebx("基地址"(base)寄存器, 在内存寻址时存放基地址。),esi,edi(源/目标索引寄存器"(source/destination index))按顺序压入栈顶,而esp也会指向栈顶 

5.lea指令,加载有效地址;将ebp-0E4h的地址放入edi中,也就是edi指向ebp-0E4h(从栈底开辟空间)

6.把39h放到ecx中 
把0cccccccch放到eax中 

初始化ecx与eax的值

EAX 是"累加器"(accumulator), 它是很多加法乘法指令的缺省寄存器。
ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。(不过这里应该就是当普通寄存器使用的)
从edi所指向的地址开始向高地址进行拷贝,拷贝的次数为ecx内容,拷贝的内容为eax内荣,即将开辟出的空间除了栈顶部分的三个寄存器的值,都初始化为0cccccccch。帧栈空间大的小一般由函数内部的给局部变量的空间决定的

之后便开始执行main()函数内的内容

当main()中调用子函数add()时,首先先将形参压栈,利用call函数记录下一个命令的地址(压栈记录)

注意:call语句push的是下一条指令的地址,为了函数返回时知道从哪儿接着执行 

接下来进入add函数: 
A.先把main函数ebp压栈,保存指向main()函数栈帧底部的ebp的地址,目的是当返回时能找到main函数栈底,此时esp指向新的栈顶位置 
将main函数的ebp压栈,也是为了返回时找到main函数栈底 
B.将esp的值赋给ebp,产生新的ebp,即Add()函数栈帧的ebp; 
C.给esp减去一个16进制数0CCh(为Add()函数预开辟空间); 
D.push ebx、esi、edi; 
E.lea指令,加载有效地址; 
F.初始化预开辟的空间为0xcccccccc; 

G.指针和引用从底层处理来看,处理方式是一致的。都是将内存的地址存入到寄存器,区别为指针会分配内存,用来存储指向的内存地址,可以通过*解引用的方式,找到指向的内存单元,自身开辟的内存单元也可以被找到。而引用,虽然在C++中底层处理时和指针处理方式相同,不过在用到引用变量的地方,系统会自动对其进行解引用,这一步骤系统默认进行,所以我们找不到引用自身开辟的内存单元,从这里看,引用好像没有开辟自身内存,只是给引用对象起了一个别名。而且初始化需要赋值。
--------------------- 
作者:WuDi_Quan 
来源:CSDN 
原文:https://blog.csdn.net/IT_Quanwudi/article/details/84549968 
版权声明:本文为博主原创文章,转载请附上博文链接!

开始执行add()函数内容

将被返回的局部变量存入寄存器(局部变量会被销毁,一般是eax)

K.接下来执行pop出栈操作,edi esi ebx依次从上向下出栈,esp 会向下移动,栈的特点:先进后出,后进先出 
L.将ebp值赋给esp,也就是esp向下移动指向ebp位置,此时add开辟的栈空间已经销毁 (栈顶栈底相同,空间销毁)
M.pop将栈顶的元素弹出放到ebp中,也就是说将main函数的ebp放入ebp中,即ebp现在指向main函数ebp (回到main帧栈)

N,完成返回值赋值工作,把之前push的地址弹出去,这时就要返回main函数,这也就是为什么之前要push这个地址,这样call指令就完成了 
接下来从那个call指令继续执行 

向高位移动esp销毁形参,完成了Add()的调用。
--------------------- 
作者:xy294636185 
来源:CSDN 
原文:https://blog.csdn.net/xy294636185/article/details/79999311 

3.压栈过程

先将栈顶指针加1,之后将要压栈的内容赋给栈顶指针所指向的空间。

4.STL vector

  1. vector是表示可变大小数组的顺序序列容器。
  2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
  3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
  4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
  5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
  6. 与其它动态序列容器相比(deques, lists and forward_lists), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起lists和forward_lists统一的迭代器和引用更好。

vector基本操作

(1). 容量

  • 向量大小: vec.size();
  • 向量最大容量: vec.max_size();
  • 更改向量大小: vec.resize();
  • 向量真实大小: vec.capacity();
  • 向量判空: vec.empty();
  • 减少向量大小到满足元素所占存储空间的大小: vec.shrink_to_fit(); //shrink_to_fit

(2). 修改

  • 多个元素赋值: vec.assign(); //类似于初始化时用数组进行赋值
  • 末尾添加元素: vec.push_back();
  • 末尾删除元素: vec.pop_back();
  • 任意位置插入元素: vec.insert();
  • 任意位置删除元素: vec.erase();
  • 交换两个向量的元素: vec.swap();
  • 清空向量元素: vec.clear();

(3)迭代器

  • 开始指针:vec.begin();
  • 末尾指针:vec.end(); //指向最后一个元素的下一个位置
  • 指向常量的开始指针: vec.cbegin(); //意思就是不能通过这个指针来修改所指的内容,但还是可以通过其他方式修改的,而且指针也是可以移动的。
  • 指向常量的末尾指针: vec.cend();

(4)元素的访问

  • 下标访问: vec[1]; //并不会检查是否越界
  • at方法访问: vec.at(1); //以上两者的区别就是at会检查是否越界,是则抛出out of range异常
  • 访问第一个元素: vec.front();
  • 访问最后一个元素: vec.back();
  • 返回一个指针: int* p = vec.data(); //可行的原因在于vector在内存中就是一个连续存储的数组,所以可以返回一个指针指向这个数组。这是是C++11的特性。

5.vector底层实现

底层实现为动态分配的数组,所以支持快速随机访问。

扩容规则为当我们新建一个vector的时候,会首先分配给他一片连续的内存空间,如std::vector<int> vec,当通过push_back或者其他插入函数向其中增加元素时,如果初始分配空间已满,就会引起vector扩容,其扩容规则在gcc下以2倍方式完成:
首先重新申请一个2倍大的内存空间;
然后将原空间的内容拷贝过来;
最后将原空间内容进行释放,将内存交还给操作系统;

注意事项:
根据vector的插入和删除特性,以及扩容规则,我们在使用vector的时候要注意,在插入位置和删除位置之后的所有迭代器和指针引用都会失效,同理,扩容之后的所有迭代器指针和引用也都会失效。(内存空间会变,插入和删除时后面的位置都挪了一位)

6.vector与list的区别

vector数据结构
vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。
因此能高效的进行随机存取,时间复杂度为o(1);
但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。

list数据结构
list是由双向链表实现的,因此内存空间是不连续的。
只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);
但由于链表的特点,能高效地进行插入和删除。

迭代器支持不同

异:vector中,iterator支持 ”+“、”+=“,”<"等操作。而list中则不支持。

同:vector<int>::iterator和list<int>::iterator都重载了 “++ ”操作。

7.堆与栈

预备知识:程序的内存分配  
  一个由C/C++编译的程序占用的内存分为以下几个部分  
  1、栈区(stack)—   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。  
  2、堆区(heap)   —   一般由程序员分配释放,   若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。  
  3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。  
  4、文字常量区 — 常量字符串就是放在这里的。   程序结束后由系统释放  
  5、程序代码区 — 存放函数体的二进制代码。

例子程序    
  这是一个前辈写的,非常详细    

  //main.cpp    
  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”  
 // 优化成一个地方。    
  }    


  二、堆和栈的理论知识    
  2.1申请方式    
  stack:    
  由系统自动分配。   例如,声明在函数中一个局部变量   int   b;   系统自动在栈中为b开辟空间    
  heap:    
  需要程序员自己申请,并指明大小,在c中malloc函数    
  如p1   =   (char   *)malloc(10);    
  在C++中用new运算符    
  如p2   =   new   char[10];    
  但是注意p1、p2本身是在栈中的。    
 
  申请后系统的响应    
  栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。    
  堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,  会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。  另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部  分重新放入空闲链表中。    
   
  2.3申请大小的限制    
  栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意  思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有  的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将  提示overflow。因此,能从栈获得的空间较小。    
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。    
   
   2.4申请效率的比较:    
  栈由系统自动分配,速度较快。但程序员是无法控制的。    

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

 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是  直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。  
   
  2.5堆和栈中的存储内容    
  栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
  堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
   
  2.6存取效率的比较      
  char   s1[]   =   “aaaaaaaaaaaaaaa”;    
  char   *s2   =   “bbbbbbbbbbbbbbbbb”;    
  aaaaaaaaaaa是在运行时刻赋值的;    
  而bbbbbbbbbbb是在编译时就确定的;    
  但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。    
  比如:    
  #include    
  void   main()    
  {    
  char   a   =   1;    
  char   c[]   =   “1234567890”;    
  char   *p   =”1234567890”;    
  a   =   c[1];    
  a   =   p[1];    
  return;    
  }    
  对应的汇编代码    
  10:   a   =   c[1];    
  00401067   8A   4D   F1   mov   cl,byte   ptr   [ebp-0Fh]    
  0040106A   88   4D   FC   mov   byte   ptr   [ebp-4],cl    
  11:   a   =   p[1];    
  0040106D   8B   55   EC   mov   edx,dword   ptr   [ebp-14h]    
  00401070   8A   42   01   mov   al,byte   ptr   [edx+1]    
  00401073   88   45   FC   mov   byte   ptr   [ebp-4],al    
  第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到  
  edx中,再根据edx读取字符,显然慢了。    


   
8.malloc与new

1. 申请的内存所在位置

new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

那么自由存储区是否能够是堆(问题等价于new是否能在堆上动态分配内存),这取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。

特别的,new甚至可以不为对象分配内存!定位new的功能可以办到这一点:

new (place_address) type

place_address为一个指针,代表一块内存的地址。当使用上面这种仅以一个地址调用new操作符时,new操作符调用特殊的operator new,也就是下面这个版本:

void * operator new (size_t,void *) //不允许重定义这个版本的operator new

这个operator new不分配任何的内存,它只是简单地返回指针实参,然后右new表达式负责在place_address指定的地址进行对象的初始化工作。

2.返回类型安全性

new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图方法自己没被授权的内存区域。关于C++的类型安全性可说的又有很多了。

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

new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功:

int *a  = (int *)malloc ( sizeof (int ));
if(NULL == a)
{
    ...
}
else 
{
    ...
}

从C语言走入C++阵营的新手可能会把这个习惯带入C++:

int * a = new int();
if(NULL == a)
{
    ...
}
else
{   
    ...
}

实际上这样做一点意义也没有,因为new根本不会返回NULL,而且程序能够执行到if语句已经说明内存分配成功了,如果失败早就抛异常了。正确的做法应该是使用异常机制:

try
{
    int *a = new int();
}
catch (bad_alloc)
{
    ...
}

如果你想顺便了解下异常基础,可以看http://www.cnblogs.com/QG-whz/p/5136883.htmlC++ 异常机制分析。

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

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

class A{...}
A * ptr = new A;
A * ptr = (A *)malloc(sizeof(A)); //需要显式指定所需内存大小sizeof(A);

当然了,我这里使用malloc来为我们自定义类型分配内存是不怎么合适的,请看下一条。

5.是否调用构造函数/析构函数

使用new操作符来分配对象内存时会经历三个步骤:

  • 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
  • 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
  • 第三部:对象构造完成后,返回一个指向该对象的指针。

使用delete操作符来释放对象内存时会经历两个步骤:

  • 第一步:调用对象的析构函数。
  • 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。

总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。而malloc则不会。如果你不嫌啰嗦可以看下我的例子:

class A
{
public:
    A() :a(1), b(1.11){}
private:
    int a;
    double b;
};
int main()
{
    A * ptr = (A*)malloc(sizeof(A));
    return 0;
}

在return处设置断点,观看ptr所指内存的内容:

可以看出A的默认构造函数并没有被调用,因为数据成员a,b的值并没有得到初始化,这也是上面我为什么说使用malloc/free来处理C++的自定义类型不合适,其实不止自定义类型,标准库中凡是需要构造/析构的类型通通不合适。

而使用new来分配对象时:

int main()
{
    A * ptr = new A;
}

查看程序生成的汇编代码可以发现,A的默认构造函数被调用了:

6.对数组的处理

C++提供了new[]与delete[]来专门处理数组类型:

A * ptr = new A[10];//分配10个A对象

使用new[]分配的内存必须使用delete[]进行释放:

delete [] ptr;

new对数组的支持体现在它会分别调用构造函数函数初始化每一个数组元素,释放对象时为每个对象调用析构函数。注意delete[]要与new[]配套使用,不然会找出数组对象部分释放的现象,造成内存泄漏。

至于malloc,它并知道你在这块内存上要放的数组还是啥别的东西,反正它就给你一块原始的内存,在给你个内存的地址就完事。所以如果要动态分配一个数组的内存,还需要我们手动自定数组的大小:

int * ptr = (int *) malloc( sizeof(int) );//分配一个10个int元素的数组

7.new与malloc是否可以相互调用

operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。下面是编写operator new /operator delete 的一种简单方式,其他版本也与之类似:

void * operator new (sieze_t size)
{
    if(void * mem = malloc(size)
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept
{
    free(mem);
}

8.是否可以被重载

opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:

//这些版本可能抛出异常
void * operator new(size_t);
void * operator new[](size_t);
void * operator delete (void * )noexcept;
void * operator delete[](void *0)noexcept;
//这些版本承诺不抛出异常
void * operator new(size_t ,nothrow_t&) noexcept;
void * operator new[](size_t, nothrow_t& );
void * operator delete (void *,nothrow_t& )noexcept;
void * operator delete[](void *0,nothrow_t& )noexcept;

我们可以自定义上面函数版本中的任意一个,前提是自定义版本必须位于全局作用域或者类作用域中。太细节的东西不在这里讲述,总之,我们知道我们有足够的自由去重载operator new /operator delete ,以决定我们的new与delete如何为对象分配内存,如何回收对象。

而malloc/free并不允许重载。

9. 能够直观地重新分配内存

使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。

new没有这样直观的配套设施来扩充内存。

10. 客户处理内存分配不足

在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler。new_handler是一个指针类型:

namespace std
{
    typedef void (*new_handler)();
}

指向了一个没有参数没有返回值的函数,即为错误处理函数。为了指定错误处理函数,客户需要调用set_new_handler,这是一个声明于的一个标准库函数:

namespace std
{
    new_handler set_new_handler(new_handler p ) throw();
}

set_new_handler的参数为new_handler指针,指向了operator new 无法分配足够内存时该调用的函数。其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要发生替换)的那个new_handler函数。

对于malloc,客户并不能够去编程决定内存不足以分配时要干什么事,只能看着malloc返回NULL。

9.C++默认函数

C++的类中有6个默认的函数,它们分别是:

构造函数
析构函数
拷贝构造函数
赋值运算符的重载函数
取地址操作符的重载函数

const修饰的取地址操作符的重载函数 
这6个默认的函数有两个特点:共有的;内联的

今天我们先来看看前三个默认的函数:构造函数、析构函数和拷贝构造函数

1.构造函数

构造函数的作用就是初始化对象的内存空间,所以调用构造函数是生成对象的一环,需要生成对象必须调用构造函数,之后再调用构造函数是通过该对象调用的。如果类定义中没有给出构造函数,则C++编译器自动产生一个默认的构造函数,只要我们自己定义了一个构造函数,系统就不会自动生成默认的构造函数。构造函数的函数名与类名相同。

注意:构造函数是不能手动调用的

2.析构函数

析构函数的作用就是:释放对象所占的其他资源(对象的空间是在栈上开辟的,这里的其他资源是指栈以外的资源,如new()开辟的堆资源等)

调用的构造函数的个数与析构函数的个数相等,并且,构造与析构的顺序是:先构造的后析构,后构造的先析构

析构函数的特点是:1.不可重载;2.可以手动调用

3.拷贝构造函数

默认的拷贝构造函数的作用:用一个已经存在的对象来生成一个相同类型的新对象,即创建对象时使用同类对象来进行初始化,这时所用的构造函数称为拷贝构造函数,拷贝构造函数是特殊的构造函数。

系统默认的拷贝构造函数是浅拷贝的,但如果类中含有指针类型的变量(如下图所示),须考虑用深拷贝来实现

4.赋值运算符的重载函数

把一个已存在的对象赋值给相同类型的已存在对象

默认的赋值运算符的重载函数也是浅拷贝的

深拷贝函数实现:

(1)判断是否为自赋值

(2)释放旧资源

(3)申请新资源

(4)赋值

(5)返回值为类类型的一个引用。

5.取地址运算符重载

取地址运算符重载分为两种:一种是普通类型的对象;一种是const修饰的常类型对象。

  被const修饰的常类型对象只能调用同样具有const属性的函数或者成员,所有在第二种重载类型的末尾加const将默认的this指针修改为const this。此时不仅函数的返回值不能被修改,函数内部的成员也不能被修改。

  小结:

1.const成员函数可以访问非const对象的非const数据成员、const数据 成员,也可以访问const对象内的所有数据成员,而不能修改它们。

2.非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不能访问const对象的任意数据成员。

3.在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const成员。


--------------------- 
作者:bbwn_ 
来源:CSDN 
原文:https://blog.csdn.net/bbwn_/article/details/51554715 
版权声明:本文为博主原创文章,转载请附上博文链接!

10.select与epoll是怎么实现的(要试试)

1. 5种IO模型:
(1)blocking IO - 阻塞IO

(2)nonblocking IO - 非阻塞IO

(3)IO multiplexing - IO多路复用

(4)signal driven IO - 信号驱动IO

(5) asynchronous IO - 异步IO

其中前面4种IO都可以归类为synchronous IO - 同步IO,signal driven IO平时用的比较少。

2.Select:
不断地轮询所负责的所有socket,当某个socket有数据到达时,就通知用户进程。(默认是1024)

(1)实现:

当用户process调用select的时候,select会将需要监控的fds集合拷贝到内核空间(假设监控的仅仅是socket可读),然后遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件,遍历完所有的sk后,如果没有任何一个sk可读,那么select会调用schedule_timeout进入schedule循环(进入延时唤醒状态),使得process进入睡眠。如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的sk集合,挨个收集可读事件并从内核态拷贝到用户态。

select为每个socket引入一个poll逻辑,该poll逻辑用于收集socket发生的事件。poll函数返回一个描述读写是否就绪的mass掩码,根据掩码对fd_set赋值

通过上面的select逻辑过程分析,select存在两个问题:

A. 被监控的fds需要从用户空间拷贝到内核空间。为了减少数据拷贝带来的性能损坏,内核对被监控的fds集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)。

B. 被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次调用sk的poll函数收集可读事件,仅仅关心是否有数据可读这样一个事件,当事件通知来的时候,由于数据的到来是异步的,我们不知道事件来的时候,有多少个被监控的socket有数据可读了,于是,只能挨个遍历每个socket来收集可读事件。

3. epoll

Int epoll_creat( int size);  //创建一个epoll fd。
Int epoll_ctl( int epfd, int op, int fd, struct epoll_evevt *event); 
Int epoll_wait( int epfd, struct epoll_evevt *events, int maxevents, int timeout);


epoll引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开。同时,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝。同时,对于高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap(内存映射)同一块内存来解决。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。

另外,epoll通过epoll_ctl来对监控的fds集合来进行增、删、改,那么必须涉及到fd的快速查找问题。在linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是epoll_create(int size)有一个参数size,以便内核根据size的大小来分配hash的大小。在linux 2.6.8以后的内核中,epoll使用红黑树来组织监控的fds集合,于是epoll_create(int size)的参数size实际上已经没有意义了。

(1)Epoll高效的原因:

A. select/poll每次调用都要将监控的fd传递给select/poll系统调用,需要从用户态拷贝到内核态,而调用epoll_wait时不需要。Epoll_creat在内核态准备数据结构存放fd,epoll_ctl对此数据结构进行监控。(内存映射)

B. 内核使用了slab机制,为epoll提供了快速的数据结构。

在内核里,一切皆文件。Epoll向内核注册了一个文件系统,用于存储上述被监控的fd。当调用epoll_creat时,就会在这个虚拟的epoll文件系统里创建一个file结点。Epoll在被内核初始化时,开辟出epoll自己的高速cache区,用来安置每一个我们想要监控的fd,以红黑树保存在内核中。

C. 当调用epoll_ctl塞入百万个fd时,epoll_wait仍然可以快速返回,并有效的将发生时间的fd给用户(建立了一个list链表,存储准备就绪的事件。)

List链表维护:epoll_ctl将fd放到epoll文件系统里file对应的红黑树外,还在内核中断处理程序注册了一个回调函数epoll_callback_sk sk,告诉内核如果该fd中断到了,就把它放到ready list链表里。Epoll_wait只需要观察该链表有没有数据,如果有数据就返回就绪链表里面的数据。

(2)Epoll的两种模式:

A. 水平触发(LT Level Triggered):使用此种模式,当数据可读的时候,epoll_wait()将会一直返回就绪事件。如果你没有处理完全部数据,并且再次在该epoll实例上调用epoll_wait()才监听描述符的时候,它将会再次返回就绪事件,因为有数据可读。

B. 边缘触发(ET Edge Triggered):使用此种模式,只能获取一次就绪通知,如果没有处理完全部数据,并且再次调用epoll_wait()的时候,它将会阻塞,因为就绪事件已经释放出来了。

ET的效能更高,但是对程序员的要求也更高。在ET模式下,我们必须一次干净而彻底地处理完所有事件。ET只支持非阻塞socket。LT两种模式的socket都支持。

sk从ready_list移除的时机正是区分两种事件模式的本质。因为,通过上面的介绍,我们知道ready_list是否为空是epoll_wait是否返回的条件。于是,在两种事件模式下,步骤5如下:

对于Edge Triggered (ET) 边沿触发: 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件

对于Level Triggered (LT) 水平触发:遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件。如果该sk的poll函数返回了关心的事件(对于可读事件来说,就是POLL_IN事件),那么该sk被重新加入到epoll的ready_list中。

对于可读事件而言,在ET模式下,如果某个socket有新的数据到达,那么该sk就会被排入epoll的ready_list,从而epoll_wait就一定能收到可读事件的通知(调用sk的poll逻辑一定能收集到可读事件)。于是,我们通常理解的缓冲区状态变化(从无到有)的理解是不准确的,准确的理解应该是是否有新的数据达到缓冲区。

而在LT模式下,某个sk被探测到有数据可读,那么该sk会被重新加入到read_list,那么在该sk的数据被全部取走前,下次调用epoll_wait就一定能够收到该sk的可读事件(调用sk的poll逻辑一定能收集到可读事件),从而epoll_wait就能返回。

对于可读事件而言,LT比ET多了两个操作:(1)对ready_list的遍历的时候,对于收集到可读事件的sk会重新放入ready_list;(2)下次epoll_wait的时候会再次遍历上次重新放入的sk,如果sk本身没有数据可读了,那么这次遍历就变得多余了。

在服务端有海量活跃socket的时候,LT模式下,epoll_wait返回的时候,会有海量的socket sk重新放入ready_list。如果,用户在第一次epoll_wait返回的时候,将有数据的socket都处理掉了,那么下次epoll_wait的时候,上次epoll_wait重新入ready_list的sk被再次遍历就有点多余,这个时候LT确实会带来一些性能损失。然而,实际上会存在很多多余的遍历么?

先不说第一次epoll_wait返回的时候,用户进程能否都将有数据返回的socket处理掉。在用户处理的过程中,如果该socket有新的数据上来,那么协议栈发现sk已经在ready_list中了,那么就不需要再次放入ready_list,也就是在LT模式下,对该sk的再次遍历不是多余的,是有效的。同时,我们回归epoll高效的场景在于,服务器有海量socket,但是活跃socket较少的情况下才会体现出epoll的高效、高性能。因此,在实际的应用场合,绝大多数情况下,ET模式在性能上并不会比LT模式具有压倒性的优势,至少,目前还没有实际应用场合的测试表面ET比LT性能更好。

(3) ET vs LT - 复杂度

我们知道,对于可读事件而言,在阻塞模式下,是无法识别队列空的事件的,并且,事件通知机制,仅仅是通知有数据,并不会通知有多少数据。于是,在阻塞模式下,在epoll_wait返回的时候,我们对某个socket_fd调用recv或read读取并返回了一些数据的时候,我们不能再次直接调用recv或read,因为,如果socket_fd已经无数据可读的时候,进程就会阻塞在该socket_fd的recv或read调用上,这样就影响了IO多路复用的逻辑(我们希望是阻塞在所有被监控socket的epoll_wait调用上,而不是单独某个socket_fd上),造成其他socket饿死,即使有数据来了,也无法处理。

接下来,我们只能再次调用epoll_wait来探测一些socket_fd,看是否还有数据可读。在LT模式下,如果socket_fd还有数据可读,那么epoll_wait就一定能够返回,接着,我们就可以对该socket_fd调用recv或read读取数据。然而,在ET模式下,尽管socket_fd还是数据可读,但是如果没有新的数据上来,那么epoll_wait是不会通知可读事件的。这个时候,epoll_wait阻塞住了,这下子坑爹了,明明有数据你不处理,非要等新的数据来了在处理,那么我们就死扛咯,看谁先忍不住。

等等,在阻塞模式下,不是不能用ET的么?是的,正是因为有这样的缺点,ET强制需要在非阻塞模式下使用。在ET模式下,epoll_wait返回socket_fd有数据可读,我们必须要读完所有数据才能离开。因为,如果不读完,epoll不会在通知你了,虽然有新的数据到来的时候,会再次通知,但是我们并不知道新数据会不会来,以及什么时候会来。由于在阻塞模式下,我们是无法通过recv/read来探测空数据事件,于是,我们必须采用非阻塞模式,一直read直到EAGAIN。因此,ET要求socket_fd非阻塞也就不难理解了。

另外,epoll_wait原本的语意是:监控并探测socket是否有数据可读(对于读事件而言)。LT模式保留了其原本的语意,只要socket还有数据可读,它就能不断反馈,于是,我们想什么时候读取处理都可以,我们永远有再次poll的机会去探测是否有数据可以处理,这样带来了编程上的很大方便,不容易死锁造成某些socket饿死。相反,ET模式修改了epoll_wait原本的语意,变成了:监控并探测socket是否有新的数据可读。

于是,在epoll_wait返回socket_fd可读的时候,我们需要小心处理,要不然会造成死锁和socket饿死现象。典型如listen_fd返回可读的时候,我们需要不断的accept直到EAGAIN。假设同时有三个请求到达,epoll_wait返回listen_fd可读,这个时候,如果仅仅accept一次拿走一个请求去处理,那么就会留下两个请求,如果这个时候一直没有新的请求到达,那么再次调用epoll_wait是不会通知listen_fd可读的,于是epoll_wait只能睡眠到超时才返回,遗留下来的两个请求一直得不到处理,处于饿死状态。

总结一下,ET和LT模式下epoll_wait返回的条件

ET - 对于读操作

A.  当接收缓冲buffer内待读数据增加的时候时候(由空变为不空的时候、或者有新的数据进入缓冲buffer)

B. 调用epoll_ctl(EPOLL_CTL_MOD)来改变socket_fd的监控事件,也就是重新mod socket_fd的EPOLLIN事件,并且接收缓冲buffer内还有数据没读取。(这里不能是EPOLL_CTL_ADD的原因是,epoll不允许重复ADD的,除非先DEL了,再ADD)

因为epoll_ctl(ADD或MOD)会调用sk的poll逻辑来检查是否有关心的事件,如果有,就会将该sk加入到epoll的ready_list中,下次调用epoll_wait的时候,就会遍历到该sk,然后会重新收集到关心的事件返回。

ET - 对于写操作

A. 发送缓冲buffer内待发送的数据减少的时候(由满状态变为不满状态的时候、或者有部分数据被发出去的时候)

B.  调用epoll_ctl(EPOLL_CTL_MOD)来改变socket_fd的监控事件,也就是重新mod socket_fd的EPOLLOUT事件,并且发送缓冲buffer还没满的时候。

LT - 对于读操作

接收缓冲buffer内有可读数据的时候

LT - 对于写操作

发送缓冲buffer还没满的时候
--------------------- 
作者:玲珑子_a 
来源:CSDN 
原文:https://blog.csdn.net/lxin_liu/article/details/89312906 
版权声明:本文为博主原创文章,转载请附上博文链接!

11.进程和线程、协程的区别

进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。表示资源分配的活动。是线程的容器。

线程:是进程的一个执行单元,是进程内可调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。

一个程序至少一个进程,一个进程至少一个线程。每个线程都有自己独立的运行栈和程序计数器(PC)。

协程:一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制

为什么会有线程?

  每个进程都有自己的地址空间,即进程空间,在网络或多用户换机下,一个服务器通常需要接收大量不确定数量用户的并发请求,为每一个请求都创建一个进程显然行不通(系统开销大响应用户请求效率低),因此操作系统中线程概念被引进。

  • 线程的执行过程是线性的,尽管中间会发生中断或者暂停,但是进程所拥有的资源只为改线状执行过程服务,一旦发生线程切换,这些资源需要被保护起来。
  • 进程分为单线程进程和多线程进程,单线程进程宏观来看也是线性执行过程,微观上只有单一的执行过程。多线程进程宏观是线性的,微观上多个执行操作。

线程的改变只代表CPU的执行过程的改变,而没有发生进程所拥有的资源的变化。 

进程线程的区别:

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
  • 两者均可并发执行。

优缺点:

  线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。

  进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。

何时使用多进程,何时使用多线程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。

要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

12.fork()内部是怎么实现的

一个现有的进程可以调用fork函数创建一个新进程。原型如下:

#include<unistd.h>  
  
pid_t fork(void);  
  
//返回值:自进程中返回0,父进程返回进程id,出错返回-1  

一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的.这取决于内核所使用的调度算法.如果要求父,子进程之间相互同步.则要求某种形式的进程间通信. 好了我们继续,当进程调用fork后,当控制转移到内核中的fork代码后,内核会做4件事情:

1.分配新的内存块和内核数据结构给子进程

2.将父进程部分数据结构内容(数据空间,堆栈等)拷贝至子进程

3.添加子进程到系统进程列表当中

4.fork返回,开始调度器调度

由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。所以fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值不同

其中父进程返回子进程pid,这是由于一个进程可以有多个子进程,但是却没有一个函数可以让一个进程来获得这些子进程id,那谈何给别人你创建出来的进程。而子进程返回0,这是由于子进程可以调用getppid获得其父进程进程ID,但这个父进程ID却不可能为0,因为进程ID0总是有内核交换进程所用,故返回0就可代表正常返回了。

从fork函数开始以后的代码父子共享,既父进程要执行这段代码,子进程也要执行这段代码.(子进程获得父进程数据空间,堆和栈的副本. 但是父子进程并不共享这些存储空间部分. (即父,子进程共享代码段.)。现在很多实现并不执行一个父进程数据段,堆和栈的完全复制. 而是采用写时拷贝技术(不懂可以戳进去看一看).这些区域有父子进程共享,而且内核地他们的访问权限改为只读的.如果父子进程中任一个试图修改这些区域,则内核值为修改区域的那块内存制作一个副本, 也就是如果你不修改我们一起用,你修改了之后对于修改的那部分内容我们分开各用个的.

父进程创建子进程时,IO缓存区的内容也会同时复制。

在重定向父进程的标准输出时,子进程标准输出也被重定向。这就源于父子进程会共享所有的打开文件。 因为fork的特性就是将父进程所有打开文件描述符复制到子进程中。当父进程的标准输出被重定向,子进程本是写到标准输出的时候,此时自然也改写到那个对应的地方;与此同时,在父进程等待子进程执行时,子进程被改写到文件show.out中,然后又更新了与父进程共享的该文件的偏移量;那么在子进程终止后,父进程也写到show.out中,同时其输出还会追加在子进程所写数据之后。

fork()底层实现

linux平台通过clone()系统调用实现fork(). fork(),vfork()和clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork(). 再然后do_fork()完成了创建中的大部分工作,他定义在kernel/fork.c当中.该函数调用copy_process(). 然后重点来了,我们看看这个copy_process函数到底做了那些事情?? 我画一张图帮我们理解:

13.指针与引用

引用和指针有什么区别?

本质:引用是别名,指针是地址,具体的:

①从现象上看,指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
②从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域,因为引用声明时必须初始化,从而指向一个已经存在的对象。引用不能指向空值。

③ 从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针
④不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
⑤理论上,对于指针的级数没有限制,但是引用只能是一级。如下:
  int** p1;         // 合法。指向指针的指针
  int*& p2;         // 合法。指向指针的引用
  int&* p3;         // 非法。指向引用的指针是非法的
  int&& p4;         // 非法。指向引用的引用是非法的
  注意上述读法是从左到右

14.两个链表的第一个公共结点

1.砍掉较长链表的头,同时遍历两个链表。

2.以AB和BA的形式构成两个新的链表,并同时遍历。

15.C语言中如何判断大端小端

在操作系统中,经常会用到判断大小端,很多面试题中也会经常遇到,以前的时候没有总结过,这里总结一下。

以后用到了就直接可以用了。

  所谓的大小端,大致的解释意思就是:

【大端模式】 CPU对操作数的存放方式是高地址存放低位,低地址存放高位。

【小端模式】CPU对操作数的存放方式是高地址存放高位,低地址存放低位。

大多数ARM处理器都是采用的小端模式,PowerPC是采用的大端模式,网络字节序是采用的大端模式。

  常用的有两种方式来判断大小端,一种是使用C语言中的联合体,具体代码如下:

1

2

3

4

5

6

7

8

9

10

int checkCPU()

{

union w

{

int a;

char b;

}c;

c.a = 1;

return (c.b == 1); // 小端返回TRUE,大端返回FALSE

}

  其中,linux内核中就是使用这部分的代码,代码如下所示:

1

2

static union char c[4]; unsigned long mylong; } endian_test = {'l''?''?''b' } };

#define ENDIANNESS ((char)endian_test.mylong)

  另外一种就是使用指针的方式,具体代码如下所示:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

void checkPoint(void)

 

{

 

    int i = 1;   

   unsigned char *pointer;   

 

   pointer = (unsigned char *)&i;   

 

   if(*pointer)   

 

   {   

 

             printf("litttle_endian");   

 

       }   

 

       else   

 

       {   

 

              printf("big endian/n");   

 

       }   

 

}

16.I/O多路复用

https://www.cnblogs.com/skiler/p/6852493.html

17.TCP粘包如何处理

一般情况将数据包头部出4个字节(0~4g一般够用了)来表示本包的长度,这样在已知包长度的情况下就可以将粘包分开。

18.struct与union的区别

一、Struct 和 Union有下列区别:

1.在存储多个成员信息时,编译器会自动给struct每个成员分配存储空间,struct 可以存储多个成员信息,而Union每个成员会用同一个存储空间,只能存储一个成员的信息。

2.都是由多个不同的数据类型成员组成,但在任何同一时刻,Union只存放了一个被先选中的成员,而结构体的所有成员都存在。

3.对于Union的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct 的不同成员赋值 是互不影响的。

19.多态

https://blog.csdn.net/qq_39412582/article/details/81628254

就答了静态多态,呜呜呜

20.strcpy、memcp、memset的区别

strcpy和memcpy主要有以下3方面的区别。 
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。 
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。 
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

strcpy和memcpy都是标准C库函数
一 、strcpy
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。已知strcpy函数的原型是:char* strcpy(char* dest, const char* src); 

char * strcpy(char * dest, const char * src) // 实现src到dest的复制 
{ 
  if ((src == NULL) || (dest == NULL)) //判断参数src和dest的有效性 
  {

      return NULL; 
  } 
  char *strdest = dest; //保存目标字符串的首地址 
  while ((*strDest++ = *strSrc++)!=’\0’); //把src字符串的内容复制到dest下 
  return strdest; 
}

memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。 
void *memcpy( void *dest, const void *src, size_t count ); 

二 memcpy() 

void *memcpy(void *memTo, const void *memFrom, size_t size) 
{ 
  if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效 
return NULL; 
  char tempFrom = (char )memFrom; //保存memFrom首地址 
  char tempTo = (char )memTo; //保存memTo首地址 
  while(size – > 0) //循环size次,复制memFrom的值到memTo中 
  *tempTo++ = *tempFrom++ ; 
  return memTo; 
} 


功能: 由memFrom所指内存区域复制size个字节到memTo所指内存区域。 
说明: memTo和memFrom所指内存区域不能重叠,函数返回指向memTo的指针.可以拿它拷贝任何数据类型的对象。

char a[10],b[5];              
memcpy(b, a, sizeof(b));             /*注意如果用sizeof(a),会造成b的内存地址溢出*/

三、 memset() 
extern void *memset(void *buffer, int c, int count); 
功能: 把buffer所指内存区域的前count个字节设置成字符 c,主要用于初始化某个内存空间。 
说明: 返回指向buffer的指针。用来对一段内存空间全部设置为某个字符 
例如:

char a[10];                        
memset(a, '\0', sizeof(a));    

 

  • 2
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值