【C++】C++ 知识点100题

 

1 多态的实现

存在虚函数的类至少有一个(多继承会有多个)一维的虚函数表叫做虚表(virtual table),属于成员,虚表的元素值是虚函数的入口地址,在编译时就已经为其在数据端分配了空间。编译器另外还为每个类的对象提供一个 虚表指针(vptr)指向虚表入口地址,属于对象成员。在实例化派生类对象时,先实例化基类,将基类的虚表入口地址赋值给基类的虚表指针,当基类构造函数执行完时,再将派生类的 虚表入口地址 赋值给 基类虚表指针(派生类和基类此时共享一个虚表指针,并没有各自都生成一个),在执行父类的构造函数。
以上是C++多态的实现过程,可以得出结论:

  • 1 有虚函数的类必存在一个虚表。
  • 2 虚表的构建:
  • 基类的虚表构建,先填上虚析构函数的入口地址,之后所有虚函数的入口地址按在类中声明顺序填入虚表;
  • 派生类的虚表构建,先将基类的虚表内容复制到派生类虚表中,如果派生类覆盖了基类的虚函数,则虚表中对应的虚函数入口地址也会被覆盖,为了后面寻址的一致性。

class Person{ 
     . . . 
 public : 
    Person (){} 
    virtual ~Person (){}; 
    virtual void speak (){}; 
    virtual void eat (){}; 
 }; 
 
class Girl : public Person{ 
     . . . 
   public : 
   Girl(){} 
   virtual ~Girl(){}; 
   virtual void speak(){}; 
   virtual void sing(){}; 

虚表构建图

 

虚函数表中有序放置了父类和子类中的所有虚函数,并且相同虚函数在类继承链中的每一个虚函数表中的偏移量都是一致的。所以确定的虚函数对应virtual table中一个固定位置n,n是一个在编译时期就确定的常量,所以,使用vptr加上对应的n,就可以得到对应的函数入口地址。C++采用的这种绝对地址+偏移量的方法调用虚函数,查找速度快执行效率高,时间复杂度为O(1)
这里概括一下虚函数的寻址过程:

1、获取类型名和函数名

2、从符号表中获得当前虚函数的偏移量

3、利用偏移量得到虚函数的访问地址,并调用虚函数。vptrn

2  C/C++的区别

C面向过程,C++面向对象。C++几乎是C的一个超集,几乎包含了C。

3 const 关键字

常变量: const 类型说明符 变量名

常引用: const 类型说明符 &引用名

常对象: 类名 const 对象名

常成员函数: 类名::fun(形参) const

常数组: 类型说明符 const 数组名[大小]

常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名

用法1:常量
取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
 用const声明的变量虽然增加了分配空间,但是可以保证类型安全
用法2:指针和常量
使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
所以出现在 * 之前的const是作为基础类型的一部分:
char *const cp; //到char的const指针
char const *pc1; //到const char的指针
const char pc2; //到const char的指针(后两个声明是等同的)
从右向左读的记忆方式:
cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
pc2 is a pointer to const char. 故
pc2的内容不可以改变,但pc2可以指向别的字符串
且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。
用法3:const修饰函数传入参数
将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数
用法4:修饰函数返回值
可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。
用法5:const修饰成员函数(c++特性)
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

 

const 和 宏的区别

【参考答案】

(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。

  而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。

(2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

 

4 malloc/free 和new/delete 区别

相同点:都可用于申请动态内存和释放内存
不同点:
简单点说,malloc只分配指定大小的堆内存空间,而new可以根据对象类型分配合适的堆内存空间,当然还可以通过重载operator new 自定义内存分配策略,其次还能够构造对象,free释放对应的堆内存空间,delete,先执行对象的析构函数,在释放对象所占空间。
malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。malloc分配时的大小是人为计算的,返回类型是void*,使用时需要类型转换,而new在分配时,编译器能够根据对象类型自动计算出大小,返回类型是指向对象类型的指针,其封装了sizeof和类型转换功能,实际上new分为两步,第一步是通过调用operator new函数分配一块合适,原始的,未命名的内存空间,返回类型也是void *,而且operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上,而malloc无能为力,第二步,调用构造函数构造对象,new将调用constructor,而malloc不能;delete将调用destructor,而free不能

5 指针和引用的区别

1、引用在创建时必须初始化,引用到一个有效对象,不是对象,不占用内存空间;而指针在定义时不必初始化,可以在定义后的任何地方重新赋值,是对象,占用内存空间。
2、指针可以是NULL,引用不行
3、引用貌似一个对象的小名,一旦初始化指向一个对象,就不能将其他对象重新赋值给该引用,这样引用和原对象的值都会被更改。

6 C++中堆和栈的区别

一、预备知识—程序的内存分配
一个由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本身是在栈中的。

2.2
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的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读取字符,显然慢了。


  • 管理方式不同
    栈是编译器管理,堆的占用和释放都是由程序员进行控制的;
  • 空间大小不同
    在32位系统下,一般堆的内存可以达到4G的空间,可以说堆内存几乎是没有限制的。但是对于栈,一般都有一定空间大小(跟编译器有关),比如在VC6下默认的栈空间大小是1M
  • 能否产生碎片不同
    对于堆来说,频繁的new/delete操作会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;
    但是对于栈来说,因为总是先进后出不存在内存块不连续的问题。
  • 生长方向不同
    堆的生长方向是向上的,即向着内存地址增加的方向;
    栈的生长方向是向下的,即向着内存地址减小的方向增长。
  • 分配方式不同
    堆总是动态分配的,需要程序员手动释放;
    栈存在静态分配和动态分配的:
    其中静态分配是由编译器完成的(比如局部变量的分配);
    动态分配是由alloca函数进行分配的(这个函数会在栈帧的调用处上分配一定空间,当调用alloca的函数返回到调用位置时,这些临时空间会被自动释放),栈的动态分配是由编译器自己进行释放的。
  • 分配效率不同:
    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,包括:分配专门的寄存器来存放栈的地址、入出栈都有专门指令,因此栈的效率会比较高。
    堆是C/C++函数库提供的,其机制非常复杂,比如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果找不到(可能是因为内存碎片过多),就可能调用系统功能区(用户模式和内核模式的切换)增加程序数据段的内存空间,如此便有机会分到足够大小的内存,然后进行返回。

7 关键字static

C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。

1.面向过程设计中的static

1.1静态全局变量

静态全局变量有以下特点:
• 该变量在全局数据区分配内存;
• 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
• 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

1.2静态局部变量

静态局部变量有以下特点:
• 该变量在全局数据区分配内存;
• 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
• 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
• 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

1.3静态函数

定义静态函数的好处:
• 静态函数不能被其它文件所用;
• 其它文件中可以定义相同名字的函数,不会发生冲突


二、面向对象的static关键字(类中的static关键字)

2.1静态数据成员

静态数据成员有以下特点:
• 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问;
• 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义;
• 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操;
• 静态数据成员主要用在各个对象都有相同的某项属性的时候
同全局变量相比,使用静态数据成员有两个优势:

  1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
  2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能

2.2静态成员函数

一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针
关于静态成员函数,可以总结为以下几点:
• 出现在类体外的函数定义不能指定关键字static;
• 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
• 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
• 静态成员函数不能访问非静态成员函数和非静态数据成员;
• 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;


在C++程序中调用被C 语言修饰的函数,为什么要加extern “C”?

extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。

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

extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等

C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

void print(int i);
void print(char c);
void print(float f);
void print(char* s);

编译为:

_print_int
_print_char
_print_float
_pirnt_string

这样的函数名,来唯一标识每个函数。C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。
C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了

当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。

而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

typedef int (*FT) (const void* ,const void*);//style of C++
 
extern "C"{
    typedef int (*CFT) (const void*,const void*);//style of C
    void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}
 
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
 
//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
 
int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C
 
void f(char* v,int sz)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
     
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}

10 什么是内存泄漏?什么是野指针?什么是内存越界?如何避免?

10.1内存泄漏

概念:用动态内存分配函数动态开辟的空间,在使用完毕后未释放,程序结束后,会导致一直占据该内存单元,直到程序结束,在现代操作系统中,一个应用程序使用的常规内存在程序终止时被释放。这表示一个短暂运行的应用程序中的内存泄漏不会导致严重后果。但是在内存非常有限的系统中都可能导致非常严重的后果,shared_ptr来避免内存泄漏,但是要正确使用

10.2野指针

“野指针”不是NULL指针,是指指向“垃圾”内存的指针。即指针指向的内容是不确定的。
产生的原因:
1)指针变量没有初始化。因此,创建指针变量时,该变量要被置为NULL或者指向合法的内存单元。
2)指针p被free之后,没有置为NULL,让人误以为p是个合法的指针。
3)指针跨越合法范围操作。不要返回指向栈内存(非静态局部变量)的指针或引用。
可能后果:

  • 若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,向其中写入数据,则这部分程序内容将被破坏,而导致程序错误。这种类型的程序错误,通常会导致segment fault和一般的保护错误。
  • 其他常见错误:返回一个基于栈分配的局部变量的地址时,一旦调用的函数返回,分配给这些变量的空间将回收,此时它们拥有的是垃圾值,如return &num,如果要使它的生命周期边长,应该将其声明为static

10.3 内存越界

存在一种情况就是调用栈溢出(stackoverflow),还有一种情况是缓冲区溢出,这两种情况都会导致安全漏洞。

10.3.1缓冲区溢出

strcpy会一直复制直到碰到\0,很多平台的栈变量是按照地址顺序倒着分配的(高地址向低地址),所以destination溢出后会先修改先前定义的变量,这样黑客就可以把is_administrator改为true,从而造成缓冲区溢出攻击,当然数组越界也可以造成类似的效果,不过现在C++都提供了越界检查的版本

// 缓冲区溢出攻击
const int MAX_LENGTH = 16;
bool is_administrator = false;
char destination[MAX_LENGTH];
std::string source = read_string_from_client(); //内容存储在缓冲区
strcpy(destination,source.c_str());

10.3.2栈溢出攻击

栈溢出攻击:在栈上分配length字节的空间,再往栈顶放上一个data。当Length十分大,会把data挤到栈空间之外,此时如果编译器不做越界检查的话,那么黑客只要用客户端送特定的length和data,就能改写服务器的任意内存(比如黑客可以修改服务器代码的机器码,注入一些JMP指令跳转到黑客想执行的函数)

// 栈溢出攻击
int length = read_int_from_client();
char buffer[length];    //栈空间分配
int data = read_int_from_client();

11 堆栈缓存的区别

1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2、堆是存放在二级缓存中,堆的首地址放在一级缓存缓存中,分配和释放会产生系统调用,由用户态进入内核态,所以速度会慢一些


12 STL 容器有哪些,常用的算法


13 如何理解智能指针,什么时候改变引用计数


14 share_ptr 与weak_ptr 的区别与联系


15 C++构造函数是否可以抛出异常

构造函数可以抛出异常。但从逻辑上和风险控制上,构造函数中尽量不要抛出异常,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生。因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用,就会造成内存泄漏。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员对象,包括其基类的成员,即执行直接基类和成员对象的析构函数


16 是否在析构函数抛出异常

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

  1. 那么当无法保证在析构函数中不发生异常时, 其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。

17 volatile 的作用


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

虽然可以对虚函数进行实调用,但程序员编写虚函数的本意应该是实现动态联编。在构造函数中调用虚函数,函数的入口地址是在编译时静态确定的,并未实现虚调用。但是为什么在构造函数中调用虚函数,实际上没有发生动态联编呢?
第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,将导致灾难的发生。
第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编

19 内存对齐的原则

1).数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。
2).结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。
3).收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。
4).sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地

20 内联函数有什么优点?内联函数和宏定义的区别。

1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
内联函数要做参数类型检查,这是内联函数跟宏相比的优势。

inline一般只用于如下情况:
(1)一个函数不断被重复调用。
(2)函数只有简单的几行,且函数不包含for、while、switch语句,递归。

21 数组与指针的区别与联系,函数指针,指针函数,指针数组,数组指针

定义不同
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。

写法不同
指针函数:int* fun(int x,int y);
函数指针:int (*fun)(int x,int y);
可以简单粗暴的理解为,指针函数的*是属于数据类型的,而函数指针的星号是属于函数名的。
再简单一点,可以这样辨别两者:函数名带括号的就是函数指针,否则就是指针函数。


原文链接:https://blog.csdn.net/luoyayun361/article/details/80428882

22 STL set 和map 都是基于什么实现的

红黑树

23 C++内存泄露及检测工具

检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。这截获住两个函数,我们就能跟踪每一 块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存



作者:夜风_3b8d
链接:https://www.jianshu.com/p/189956c94cef

 

20、const 符号常量;

(1)const char *p

(2)char const *p

(3)char * const p

说明上面三种描述的区别;

【参考答案】

(1)const char *p (2)char const *p 都是指向char类型的const对象指针,p不是常量,我们可以修改p的值,使其指向不同的char。(3)char* const p 此时*p可以修改,而p不能被修改。

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

 

21、用C++写个程序,如何判断一个操作系统是16位还是32位的?

【标准答案】

定义一个指针p,打印出sizeof(p),如果节后是4,则表示该操作系统是32位,打印结果是2,表示是16位。

22、用C++写个程序,如何判断一个操作系统是16位还是32位的?不能用 sizeof() 函数。

【参考答案】    

 

int a = ~0;

if(a > 65536) {

    cout<<"32 bit"<<endl;

} else {

    cout<<"16 bit"<<endl;

}

int a = ~0;//这句话什么意思啊//~按位取反运算,结果为(11111111111111111111111111111111)

 

23、识别函数或指针

void * ( * (*fp1)(int))[10]; 

float (*(* fp2)(int,int,int))(int);

int (* ( * fp3)())[10](); 

分别表示什么意思?

1、void * ( * (*fp1)(int))[10];  fp1是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个void*型指针。

2、float (*(* fp2)(int,int,int))(int);  fp2是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值是一个指针,这个指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。

3、int (* ( * fp3)())[10]();  fp3是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是int型。    

 

29、float a,b,c , 问等式 (a+b)+c==(b+a)+c 和 (a+b)+c==(a+c)+b 能否成立?

【参考答案】

两者都不行。在比较float或double时,不能简单地比较。由于计算误差,相等的概率很低。应判断两数之差是否落在区间(-e,e)内。

这个e应比浮点数的精度大一个数量级。

33、重复多次 fclose 一个打开过一次的 FILE *fp 指针会有什么结果,并请解释。 

【参考答案】

考察点:导致文件描述符结构中指针指向的内存被重复释放,进而导致一些不可预期的异常。

36、为什么需要使用堆,使用堆空间的原因?

【参考答案】

直到运行时才知道一个对象需要多少内存空间;不知道对象的生存期到底有多长。

37、 const关键字?有哪些作用?

【参考答案】

const关键字至少有下列n个作用:

(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值

(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量

(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”

注: 这个题可以考查面试者对程序设计知识的掌握程度是初级、中级还是比较深入,没有一定的知识广度和深度,不可能对这个问题给出全面的解答。大多数人只能回答出 static 和 const 关键字的部分功能。

 

39、面向对象的三个基本特征,并简单叙述之?

【参考答案】                                                                     

1. 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected, public)                  

2. 继承:广义的继承有三种实现形式:

实现继承(指使用基类的属性和方法而无需额外编码的能力)、

可视继承(子窗体使用父窗体的外观和实现代码)、

接口继承(仅使用属性和方法,实现滞后到子类实现)。

前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。                                 

3. 多态:是将父对象设置成为和一个或更多的与他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作

简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

 

40、重载(overload)、重写(override,有的书也叫做“覆盖”)、重定义(redefinition)的区别?

【标准答案】

重载    同一名字空间  是指允许存在多个同名函数,而这些函数的参数表不同

重定义/隐藏  不同名字空间 用于继承,派生类与基类的函数同名,屏蔽基类的函数

重写/覆盖 不同名字空间用于继承,子类重新定义父类虚函数的方法

 

41、多态的作用?

【参考答案】

主要是两个:

1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;

2. 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。

 

42、当一个类A 中没有声命任何成员变量与成员函数,这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。

【标准答案】

S.izeof(A)=1;
编译器不允许一个类的大小为0,会为它分配1字节的内存。试想,若,不这样做,那2个类A的实例在内存中将会无法区分。

 一个空类对象的大小是1byte。这是被编译器安插进去的一个字节,这样就使得这个空类的两个实例得以在内存中配置独一无二的地址。

 

75,C++程序下列说法正确的有: 
    A,对调用的虚函数和模板类都进行迟后编译. 
    B,基类与子类中函数如果要构成虚函数,除了要求在基类中用virtual   声名,而且必须名字相同且参数类型相同返回类型相同 
    C,重载的类成员函数都必须要:或者返回类型不同,或者参数数目不同,或者参数序列的类型不同. 
    D,静态成员函数和内联函数不能是虚函数,友员函数和构造函数也不能是虚函数,但是析构函数可以是虚函数. 
答:D

解释:A 虚函数时滞后绑定 ,B只有返回类型相同的话不能构成虚函数,C 只有返回值类型相同不能构成重载。D正确
 

55、C++中为什么用模板类?

【参考答案】

(1)可用来创建动态增长和减小的数据结构

(2)它是类型无关的,因此具有很高的可复用性。

(3)它在编译时而不是运行时检查数据类型,保证了类型安全

(4)它是平台无关的,可移植性

(5)可用于基本数据类型

56、函数模板与类模板有什么区别?

【参考答案】

函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。

58、请你谈谈你在类中如何使用 const 的。

【参考答案】

有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用 const 修饰数据成员来实现。

const 数据成员的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。 不能在类声明中初始化 const 数据成员。

const 数据成员的初始化只能在类构造函数的初始化表中进行。

61、基类的析构函数不是虚函数,会带来什么问题?

【参考答案】

派生类的析构函数用不上,会造成资源的泄漏。

 

62、main 函数执行以前,还会执行什么代码?

【参考答案】

全局对象的构造函数会在main 函数之前执行。

 

67、在不用第三方参数的情况下,交换两个参数的值。

【参考答案】

a = a + b; 

b = a – b; a = a – b;

76、以下三条输出语句分别输出什么?

 

char str1[]       = "abc";  

char str2[]       = "abc";  

const char str3[] = "abc";   

const char str4[] = "abc";   

const char* str5  = "abc";  

const char* str6  = "abc";  

cout << boolalpha << ( str1==str2 ) << endl; // 输出什么?  

cout << boolalpha << ( str3==str4 ) << endl; // 输出什么?  

cout << boolalpha << ( str5==str6 ) << endl; // 输出什么?  

复制代码

【参考答案】

分别输出 false,false,true。

str1和str2都是字符数组,每个都有其自己的存储区,它们的值则是各存储区首地址,不等;

str3和str4同上,只是按const语义,它们所指向的数据区不能修改。

str5和str6并非数组而是字符指针,并不分配存储区,其后的“abc”以常量形式存于静态数据区,而它们自己仅是指向该区首地址的指针,相等。


 

79、以下代码中的输出语句输出0吗,为什么?

 

struct CLS  

{  

      int m_i;  

      CLS( int i ) : m_i(i) {}  

      CLS()  

      {  

          CLS(0);  

      }  

};  

CLS obj;  

cout << obj.m_i << endl;

复制代码

【标准答案】

不能。在默认构造函数内部再调用带参的构造函数属用户行为而非编译器行为,亦即仅执行函数调用,而不会执行其后的初始化表达式。只有在生成对象时,初始化表达式才会随相应的构造函数一起调用。

 

84、一个栈的入栈序列是 A,B,C,D,E,则栈的不可能的输出序列是?

A、EDCBA;

B、DECBA;    

C、DCEAB;

D、ABCDE 

【标准答案】C

85、写出判断ABCD四个表达式的是否正确, 若正确, 写出经过表达式中 a 的值。

int a = 4;  

(A)、a += (a++);

(B)、a += (++a) ;  

(C)、(a++) += a;

(D)、(++a) += (a++);  

a = ?

复制代码

【参考答案】

C错误,左侧不是一个有效变量,不能赋值,可改为 (++a) += a; 改后答案依次为 9,10,10,11



https://www.jianshu.com/p/30ae4e1fbe9e

 

posted on 2022-10-04 01:27  bdy  阅读(21)  评论(0编辑  收藏  举报

导航