C++面试经典

C/C++常见面试题

变量的声明和定义有什么区别

  变量的定义为变量分配地址和存储空间,变量的声明不分配地址。一个变量可以在多个地方声明,但是只在一个地方定义。加入extern修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

  • 注意:声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分配存储空间。
  • 用static来声明一个变量的作用:
      1)对于局部变量用static声明,则是为该变量分配的空间在整个程序的执行期内都始终存在。
      2)外部变量用static来声明,则该变量的作用只限于本文件模块。
  • 示例:
int main() 
{
 extern int A;
 //这是个声明而不是定义,声明A是一个已经定义了的外部变量
 //注意:声明外部变量时可以把变量类型去掉如:extern A;
 dosth(); //执行函数
}
int A; //是定义,定义了A为整型的外部变量

简述#ifdef、#else、#endif和#ifndef的作用

  利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。

#ifdef MATH
#include "math.c"
#endif

  在子程序前加上标记,以便于追踪和调试。

#include <stdio.h>
#define CONFIG_DEBUG 
int main(){
    FILE *fp;
    fp=fopen("D:\\DEV\\test.txt","r"); 
    if(NULL==fp){
        printf("error!");
    }
#ifdef CONFIG_DEBUG 
    printf("open test.txt ok");
#endif
    return 0;
}

  应对硬件的限制。由于一些具体应用环境的硬件不一样,限于条件,本地缺乏这种设备,只能绕过硬件,直接写出预期结果。
  注意:虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长((因为所有语句都编译),运行时间长(因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。

写出int 、bool、 float 、指针变量与 “零值”比较的if 语句

//int与零值比较
if ( n == 0 )
if ( n != 0 )

//bool与零值比较
if (flag) // 表示flag为真
if (!flag) // 表示flag为假

//float与零值比较
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON) //EPSINON是允许的误差(即精度)

//指针变量与零值比较
if (p == NULL)
if (p != NULL)

结构体可以直接赋值吗

  声明时可以直接初始化,同一结构体的不同对象之间也可以直接赋值,但是当结构体中含有指针“成员”时一定要小心。注意:当有多个指针指向同一段内存时,某个指针释放这段内存可能会导致其他指针的非法操作。因此在释放前―定要确保其他指针不再使用这段内存空间。

sizeof 和strlen 的区别

  1、sizeof是一个操作符,strlen是库函数。
  2、sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数。
  3、编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
  4、数组做sizeof的参数不退化,传递给strlen就退化为指针了

C语言的关键字 static 和 C++的关键字 static 有什么区别

C语言中static关键字的作用:
  1.修饰变量:变量又分为全局变量和局部变量。
   (1).修饰全局变量
    作用域仅限于变量被定义的文件中,其他文件即使用extern 声明也没法使用此变量。
   (2).修饰局部变量
    在函数内定义的局部变量被修饰,可以延长变量的生命周期,但是作用域不变。
  2.修饰函数
   和修饰全局变量的效果类似,表明函数的作用域仅限于被定义的文件中,不必担心与其它文件中的函数同名。
C++兼容C语言,所以static在C语言中的用法,在C++中同样适用。除此之外,C++中的static关键字还有以下作用:
  1.修饰类的数据成员
   经过static关键字修饰的数据成员就成为了静态数据成员,不属于任何一个对象,而是经过此类定义的所有对象共有此静态数据成员。但是静态数据成员必须得在类外进行初始化。优点是可以实现信息隐藏,访问权限设为private,全局变量做不到这一点。
  2.修饰类的成员函数
   经过static关键字修饰的成员函数就成了静态成员函数,不属于任何一个对象,而是属于类的方法,所以静态成员函数没有隐含的this指针,不能被const所修饰,在调用时可以通过类名和作用域限定符的方式调用,也可以通过对象进行调用。
  注意
   1、static成员函数可以调用static数据成员。
   2、static成员函数不可以调用非static数据成员。
   3、static成员函数可以调用static成员函数。
   4、static成员函数不可以调用非static成员函数。
   5、非static成员函数可以调用static成员函数。

C语言的 malloc 和 C++中的 new 有什么区别

  • new 、delete是操作符,可以重载,只能在C++中使用。malloc、free是函数,可以覆盖,C、C++中都可以使用。
  • new可以调用对象的构造函数,对应的delete调用相应的析构函数。malloc仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数。
  • new 、delete返回的是某种数据类型指针,malloc、free返回的是void 指针。
  • new能自动计算需要分配的内存空间,而malloc需要手工计算字节数。
  • 注意:
      malloc申请的内存空间要用free释放,而new申请的内存空间要用delete释放,不要混用。
      delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该先该指针指向NULL。

写一个 “标准”宏MIN

# define min(a , b) ((a) < = (b) ? (a) : (b))

++i和i++的区别

  ++ i 先自增1,再返回,i++先返回i , 再自增1

volatile有什么作用

volatile在C++中的作用
  1、状态寄存器一类的并行设备硬件寄存器。
  2、一个中断服务子程序会访问到的非自动变量。
  3、多线程间被几个任务共享的变量。当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
  注意:虽然volatile在嵌入式方面应用比较多,但是在PC软件的多线程中,volatile修饰的临界变量也是非常实用的。如果一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去访问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的由于编译器优化所导致的灾难性问题。所以多线程中必须要共享的基本变量一定要加上volatile修饰符。

一个参数可以既是const又是volatile吗

  可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
  注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。

*a和&a有什么区别

  &a :其含义就是“变量a的地址”。
   ∗ * a : 用在不同的地方,含义不一样 。
    在声明语句中, ∗ * a只说明a是一个指针变量,如int ∗ * a;
    在其他语句中, ∗ * a前面没有操作数且a是一个指针时, ∗ * a代表指针a指向的地址内存放的数据,如b= ∗ * a;
     ∗ * a前面有操作数且a是一个普通变量时,a代表乘以a,如c= b ∗ a b*a ba

指针与引用的区别

 (1)指针是实体,引用是别名,没有空间。
 (2)引用定义时必须初始化,指针不用。
 (3)指针可以改,引用不可以。(因此没有int & const a,因为const本来就不可以修改;但是有int const &a,表明不能通过a修改某变量)
 (4)引用不能为空,指针可以。
 (5)sizeof(引用)计算的是它引用的对象的大小,而sizeof(指针)计算的是指针本身的大小。
 (6)不能有NULL引用,引用必须与一块合法的存储单元关联。
 (7)给引用赋值修改的是该引用与对象所关联的值,而不是与引用关联的对象。
 (8)如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
 (9)对引用的操作即是对变量本身的操作。

用C 编写一个死循环程序

while(1) 
{ } 

  注意:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。

结构体内存对齐问题

  请写出以下代码的输出结果:

#include<stdio.h>
struct S1
{
 int i:8;
 char j:4;
 int a:4;
 double b;
};
struct S2
{
 int i:8;
 char j:4;
 double b;
 int a:4;
};
struct S3
{
 int i;
 char j;
 double b;
 int a;
};
int main()
{
 printf("%d\n",sizeof(S1)); 
 printf("%d\n",sizeof(S2); 
 printf("%d\n",sizeof(S3)); 
 return 0;
}

结果
  说明:结构体作为―种复合数据类型,其构成元素既可以是基本数据类型的变量,也可以是一些复合型类型数据。对此,编译器会自动进行成员变量的对齐以提高运算效率。默认情况下,按自然对齐条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同,向结构体成员中size最大的成员对齐。许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,而这个k则被称为该数据类型的对齐模数

//c语言中没有string类型,c++中有
x86
sizeof(char)  1
sizeof(char*)  4
sizeof(int)  4
sizeof(int*)  4
sizeof(double)  8
sizeof(double*)  4
sizeof(float)  4
sizeof(float*)  4
sizeof(string)  28
sizeof(string*)  4

x64
sizeof(char)  1
sizeof(char*)  8
sizeof(int)  4
sizeof(int*)  8
sizeof(double)  8
sizeof(double*)  8
sizeof(float)  4
sizeof(float*)  8
sizeof(string)  40
sizeof(string*)  8

  指针的大小只与操作系统有关

全局变量和局部变量有什么区别?实怎么实现的?操作系统和编译器是怎么知道的?

  全局变量是整个程序都可访问的变量,谁都可以访问,生存期在整个程序从运行到结束(在程序结束时所占内存释放);
  局部变量存在于模块(子程序,函数)中,只有所在模块可以访问,其他模块不可直接访问,模块结束(函数调用完毕),局部变量消失,所占据的内存释放。
  操作系统和编译器,可能是通过内存分配的位置来知道的,全局变量分配在全局数据段并且在程序开始运行的时候被加载.局部变量则分配在堆栈里面。

简述C、C++程序编译的内存分配情况

  从静态存储区域分配:
    内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static变量,常量字符串等。
  在栈上分配:
    在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。大小为2M。
  从堆上分配:
    即动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
  一个C、C++程序编译时内存分为5大存储区:栈区、堆区、全局区、文字常量区、程序代码区
c++内存分配

简述strcpy、sprintf 与memcpy 的区别

  操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, 目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
  执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
  实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
  注意 :strcpy、sprintf与memcpy都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能 。

/*sprintf用法示例*/
#include <stdio.h>
#include <math.h>
int main()
{
   char str[80];
   sprintf(str, "Pi 的值 = %f", M_PI);
   puts(str);
   return(0);
}
/*输出:Pi 的值 = 3.141593*/

typedef 和define 有什么区别

  • 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义 常量,以及书写复杂使用频繁的宏。
  • 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
  • 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在define 声明后的引用 都是正确的。
  • 对指针的操作不同:typedef 和define 定义的指针时有很大的区别。
#include <stdio.h>

#define INT_D int*
typedef int* int_p;
int main()
{
        printf("hello world\n");
        INT_D a,b;//b不是指针,编译报错
        int_p c,d;

        int num=8;
        a=&num;
        b=&num;
        c=&num;
        d=&num;

        return 0;
}


  • 注意 :typedef定义是语句,因为句尾要加上分号。而define不是语句,千万不能在句尾加分号。

指针常量与常量指针区别

1.指针常量与常量指针的概念
  指针常量=指针本身是常量,换句话说,就是指针里面所存储的内容(内存地址)是常量,不能改变。但是,内存地址所对应的内容是可以通过指针改变的。
  常量指针=指向常量的指针,换句话说,就是指针指向的是常量,它指向的内容不能发生改变,不能通过指针来修改它指向的内容。但是,指针自身不是常量,它自身的值可以改变,从而指向另一个常量。
2.指针常量与常量指针的声明
  指针常量:数据类型 ∗ * const 指针变量。
  常量指针:数据类型 const ∗ * 指针变量 或者 const 数据类型 ∗ * 指针变量。
  常量指针常量:数据类型 const ∗ * const 指针变量 或者 const 数据类型 ∗ * const 指针变量。
3.例子:

/*指针常量的例子*/ 
int a,b; 
int * const p; 
p = &a;//正确 
p = &b;//错误 
*p = 20;//正确 

/*常量指针的例子*/ 
int a,b; 
int const *p; 
p = &a;//正确 
p = &b;//正确 
*p = 20;//错误 

简述队列和栈的异同

  队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
  注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。堆区和栈区是程序的不同内存存储区域。

设置地址为0x67a9 的整型变量的值为0xaa66

int *ptr; 
ptr = (int *)0x67a9; 
*ptr = 0xaa66; 

  注意:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。

编码实现字符串转化为整数

//剑指offer代码
#include<iostream>
using namespace std;

enum Status {KValid=0,KInvalid};
int g_nStatus = KValid;

int StrToIntCore(const char* digit, bool minus)
{
	long long num=0;

	while (*digit!='\0')
	{
		if (*digit >= '0'&&*digit <= '9')
		{
			int flag = minus ? -1 : 1;
			num = num * 10 + flag*(*digit - '0');

			if ((!minus&&num > 0x7FFFFFFF) || (minus&&num < (signed int)0x80000000))//越界
			{
				num = 0;
				break;
			}
			*digit++;
		}
		else//非数字字符
		{
			num = 0;
			break;
		}
	}
	if (*digit == '\0')//转换合法
	{
		g_nStatus = KValid;
	}

	return num;
}

int StrToInt(const char* str)
{
	g_nStatus = KInvalid;
	long long number = 0;

	if (str != NULL&&*str!='\0')
	{
		bool minus = false;//是否为负数
		if (*str == '+') str++;
		else if (*str == '-')
		{
			minus = true;
			str++;
		}
		if (*str != '\0')
		{
			number = StrToIntCore(str, minus);
		}

	}
	return (int)number;
}

int main()
{
	const char *str = "-12345";
	if (g_nStatus == KValid)
		cout << StrToInt(str) << endl;
	else
		cout << "Invalid" << endl;
	return 0;
}

C语言的结构体和C++的类有什么区别

  C语言的结构体是不能有函数成员的,而C++的类可以有。
  C语言的结构体中数据成员是没有private、public和protected访问限定的。而C++的类的成员有这些访问限定。
  C语言的结构体是没有继承关系的,而C++的类却有丰富的继承关系。
  注意:虽然C的结构体和C++的类有很大的相似度,但是类是实现面向对象的基础。而结构体只可以简单地理解为类的前身。

C++中struct和class的区别

在C++中, class和struct做类型定义是只有两点区别:
  1.默认继承权限不同,class继承默认是private继承,而struct默认是public继承
  2.class还可用于定义模板参数,像typename,但是关键字struct不能同于定义模板参数
  C++保留struct关键字,原因:
    保证与C语言的向下兼容性,C++必须提供―个struct
    C++中的struct定义必须百分百地保证与C语言中的struct的向下兼容性,把C++中的最基本的对象单元规定为class而不是struct,就是为了避免各种兼容性要求的限制
    对struct定义的扩展使C语言的代码能够更容易的被移植到C++中

如何避免“野指针”

  指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
  指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL
  指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。

句柄和指针的区别和联系是什么?

  句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。句柄是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念 。指针可以直接对内存数据进行操作,而句柄需要通过操作系统来对某些特定的,操作系统允许的数据进行操作。
句柄与指针

说一说extern“C”

  extern"C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern" C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern" C"就是其中的一个策略。
  使用:
    C++代码调用C语言代码
    在C++的头文件中使用
    在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?

答案与分析:
  C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。

#ifdef __cplusplus /* 如果采用了C++,如下代码使用C编译器 */
    extern "C" { /* 如果没有采用C++,顺序预编译 */
#endif
/* 采用C编译器编译的C语言代码段 */
#ifdef __cplusplus /* 结束使用C编译器 */
    }
#endif

C++中类成员的访问权限

  C++通过public、 protected、 private三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。在类的内部(定义类的代码内部),无论成员被声明为public、protected还是private,都是可以互相访问的,没有访问权限的限制。在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、protected属性的成员

什么是右值引用,跟左值又有什么区别?

左值和右值的概念:
  左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
  右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;区别:
  左值能寻址,右值不能;左值能赋值,右值不能;
  左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);
转载大佬的左右值讲解:非常详细易懂

面向对象的三大特征

  • 封装性:将客观事物抽象成类,每个类对自身的数据和方法实行protection (private , protected ,public )。
  • 继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有的特性基础上进行扩展,增加功能,这样产生新的类,称作是派生类。继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用
  • 多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值 之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
  • 面向对象

说一说c++中四种cast转换

大神链接
C++中四种类型转换是:static_cast , dynamic_cast ,const_cast ,reinterpret_cast
 1.const_cast < type-id > ( expression ):
  主要是用来去掉const属性,也可以加上const属性。
  去掉const属性:const_case<int*> (&num),因为不能把一个const变量直接赋给一个非const变量,必须要转换。
  加上const属性:const int* k = const_case<const int*>(j),少用,可以把一个非const变量直接赋给一个const变量,比如:const int* k = j;
使用范围:1、常量指针被转化成非常量指针,转换后指针指向原来的变量(即转换后的指针地址不变)。2、常量引用转为非常量引用。3、常量对象(或基本类型)不可以被转换成非常量对象(或基本类型)。
图片对上述运行结果的解释:如果将常量指针转换为非常量指针,编译是可以通过的。但是,如果想通过这个非常量指针去改变常量指针所指内容,是无效的
 2. static_cast
  用于各种隐式转换,比如非const转const,void ∗ * 转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
 3、dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
  向上转换:指的是子类向基类的转换
  向下转换:指的是基类向子类的转换
 它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
 4、reinterpret_cast 几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
 5、为什么不使用C的强制转换((类型说明符)(表达式))
  C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,编译器不容判断其正确性,且在代码中无法快速定位使用强制转换的位置,容易出错。

C++的空类有哪些成员函数

缺省构造函数、缺省拷贝构造函数、缺省析构函数、缺省赋值运算符、缺省取址运算符、缺省取址运算符const 。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

对c++中的smart pointer四个智能指针:shared_ptr,unique_ptr,weak_ptr,auto_ptr的理解

为什么要使用智能指针:解决内存泄漏
  智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。
  使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
  智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
这个大佬写的非常好
shared_ptr的实现

谈谈你对拷贝构造函数和赋值运算符的认识

 拷贝构造函数和赋值运算符重载有以下两个不同之处:
  1.拷贝构造函数生成新的类对象,而赋值运算符不能。
  2.由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉。
  注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。

在C++中,使用new申请的内存能否用free?使用malloc申请的内存能否通过delete释放?

 不能,malloc/free主要为了兼容C, new和delete完全可以取代malloc/free的。malloc/free的操作对象都是必须明确大小的。而且不能用在动态类上。new和delete会自动进行类型检查和大小, malloc/free不能执行构造函数与析构函数,所以不能用在动态类上。
 从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

用C++设计一个不能被继承的类

请移步
 c11之前:

///
// Define a class which can't be derived from
///
template <typename T> 
class MakeFinal
{
      friend T;
 
private :
      MakeFinal() {}
      ~MakeFinal() {}
};
 
class FinalClass2 : virtual public MakeFinal<FinalClass2>
{
public :
      FinalClass2() {}
      ~FinalClass2() {}
};

 FinalClass2不能被继承,因为其子类不是MakeFinal类的友元函数,无法调用其构造函数及析构函数。
 c11之后,使用final:

struct A
{
    virtual void foo() final;
};
 
struct B final : A
{
    void foo(); // Error: foo cannot be overridden as it's final in A
};
 
struct C : B // Error: B is final
{
};

C++自己实现一个String类

#include<iostream>
#include <cstring>
using namespace std;
class  String
{
public:
	// 默认构造函数
	String(const char *str = nullptr);
	// 拷贝构造函数
	String(const String &str);
	// 析构函数
	~String();
	// 字符串赋值函数
	String& operator=(const String &str);
	void Prints()
	{
		for (int i = 0; i < msize; i++)
		{
			cout << mystr[i];
		}
		cout << endl;
	}
private:
	char *mystr;
	int msize;
};
String::String(const char *str)//得分点:输入参数为const型
{
	if (str == nullptr)
	{
		msize = 0;
		mystr = new char[1];
		mystr[0] = '\0';// 得分点:对空字符串自动申请存放结束标志'\0'的
	}
	else
	{
		msize = strlen(str);
		mystr = new char[msize + 1];
		strcpy(mystr, str);
	}
}
String::String(const String& str)
{
	msize = str.msize;
	mystr = new char[msize + 1];//加分点:对mystr加NULL 判断
	strcpy(mystr, str.mystr);
}
String::~String()
{
	delete[] mystr;
}
String& String::operator=(const String &str)
{
	if (this == &str) return *this;//得分点:检查自赋值
	delete[] mystr;//得分点:释放原有的内存资源
	msize = strlen(str.mystr);
	mystr = new char[msize + 1];
	strcpy(mystr, str.mystr);
	return *this;//得分点:返回本对象的引用
}

访问基类的私有虚函数

写出以程序的输出结果:

#include <iostream> 
using namespace std;
class A
{
	virtual void g()
	{
		cout << "A::g" << endl;
	}
private:
	virtual void f()
	{
		cout << "A::f" << endl;
	}
};
class B : public A
{
	void g()
	{
		cout << "B::g" << endl;
	}
	virtual void h()
	{
		cout << "B::h" << endl;
	}
};
typedef void(*Fun)(void);//定义一个函数指针
void main()
{
	B b;
	Fun pFun;
	for (int i = 0; i < 3; i++)
	{
		pFun = (Fun)*((int*) * (int*)(&b) + i);
		pFun();
	}
}

结果:
结果
分析:
A的虚函数表:

A::g
A::f

B的虚函数表:

B::g
B::h

首先在for循环内,使用函数指针获得B类对象b的首地址,因为b的首地址保存了一个指针,这个指针指向了这个类的虚函数表,而这个虚函数表包含了父类A的虚函数表,当子类覆盖了父类中的虚函数时(此例子中的virtual void g()),那么b的虚函数表中原本指向父类的虚函数g()就改指向子类b的g()了。
其中要注意的是,父类的虚函数表的函数顺序是按照声明的顺序,并且父类的虚函数在子类的虚函数前面。因此对象stu的首地址指向的就是被自己覆盖的name()函数了。
虚函数表解析

对虚函数和多态的理解

 多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。举个例子:一个父类类型的指针指向一个子类对象时候,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写时候不需要加virtual也是虚函数。
 虚函数的实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。

简述类成员函数的重写、重载和隐藏的区别

(1)重写和重载主要有以下几点不同。
 范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
 参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
 virtual的区别:重写的基类中被重写的函数必须要有virtual修饰,而重载函数和被重载函数可以被virtual修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
 与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
 参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被virtual修饰,基类的函数都是被隐藏,而不是被重写。
注意:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
重写、重载、重定义
 父类如果声明了一个虚函数,子类声明了一个函数名一样的函数,会导致子类的函数不可访问。

#include <iostream> 
using namespace std;
class B {
public:
	virtual void m(int x) { cout << "B:m" << endl; }
};
class A :public B
{
	void m(){ cout << "A:m" << endl; }
};
int main()
{
	B* b;
	A a;
	b = new A();
	b->m(2);
	a.m();//无法编译,显示m不可访问
	a.B::m(1);
	return 0;
}

 子类隐藏父类之后,父类和子类都无法直接调用被隐藏的函数
遮蔽

链表和数组有什么区别

 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
 数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
 数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
 越界问题:链表不存在越界问题,数组有越界问题。
注意:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间

vector的底层原理

 vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围, end_of_storage是整块连续空间包括备用空间的尾部。当空间不够装下数据(vec.push_ back(val))时,会自动申请另一片更大的空间( 1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间[vector内存增长机制]。当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。

vector中的reserve和resize的区别

  • resize()函数的作用是改变vector元素个数
    resize(n,m)第二个参数可以省略
      n代表改变元素个数为n,m代表初始化为m .
    主要有三层含义:
      1.如果n比vector容器的size小,结果是size减小到n,然后删除n之后的数据。
      2.如果n比vector容器的size大比容器的capacity小,结果是增加size,并初始化----如果指定了,初始化为指定值,没指定初始化为缺省值,capacity不变。
      3.如果n比vector容器中的capacity大,结果是先增加容量,然后增加size,并初始化。capacity和size均改变。
  • reverse()函数的作用是改变容量
    reverse(n)
      1.如果n的大小比vector的容量大,增容到n。size不变。
      2.如果n的大小比vector的容量小。容量没有变化。size也没有变。
  • 总结:
      resize()函数是改变容器中元素个数,并初始化。------配合v1[i]来使用。因为这些位置已经初始化了,因此要用赋值。
      reserve() 函数只是改变容量。----------配合push_back使用。

vector中的size和capacity的区别

 size表示当前vector中有多少个元素(finish - start);capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start);

vector中erase方法与algorithn中的remove方法区别

 vector中erase方法真正删除了元素,迭代器不能访问了。remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除

vector迭代器失效的情况

 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it)。

正确释放vector的内存(clear(), swap(), shrink_to_fit())

 vec.clear():清空内容,但是不释放内存。
 vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
 vec.shrink_to_fit():请求容器降低其capacity和size匹配。
 vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。

list的底层原理

 list的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。
 list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取

什么情况下用vector,什么情况下用list,什么情况下用deque

 vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
 list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
 需要从首尾两端进行插入或删除操作的时候需要选择deque。

priority_queue的底层原理

 priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最
高的那一个。

map 、set、multiset、multimap的底层原理

 map、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树, linux系统中CFS进程调度算法,也用到红黑树。红黑树的特性︰
  每个结点或是红色或是黑色;
  根结点是黑色;
  每个叶结点是黑的;
  如果一个结点是红的,则它的两个儿子均是黑色;
  每个结点到其子孙结点的所有路径上包含相同数目的黑色结点

为何map和set的插入删除效率比其他序列容器高

 因为不需要内存拷贝和内存移动

为何map和set每次Insert之后,以前保存的iterator不会失效?

 因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?

 RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。

map 、set、multiset、multimap的特点

 set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
 map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。
 map和set的增删改查速度为都是logn,是比较高效的。

为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?

 存储的是结点,不需要内存拷贝和内存移动。
 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。

为何map和set不能像vector一样有个reserve函数来预分配数据?

 在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。

set的底层实现实现为什么不用哈希表而使用红黑树?

 set中元素是经过排序的,红黑树也是有序的,哈希是无序的
 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(lgn)

hash_map与map的区别?什么时候用hash_map,什么时候用map?

 构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。
 存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层
 总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
 如果考虑效率,特别当元素达到一定数量级时,用hash_map。
 考虑内存,或者元素数量较少时,用map。

迭代器失效的问题

插入操作:
  对于vector和string,如果容器内存被重新分配,iterators、pointers、references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
  对于deque,如果插入点位于除front和back的其它位置,iterators、pointers、references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
  对于list和forward_list,所有的iterator,pointer和refercnce有效。
删除操作:
  对于vector和string,删除点之前的iterators、pointers、references有效;off-the-end迭代器总是失效的;
  对于deque,如果删除点位于除front和back的其它位置,iterators、pointers、references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators、pointers、references有效;
  对于list和forward_list,所有的iterator、pointer和refercnce有效。
  对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。

STL线程不安全的情况

  在对同一个容器进行多线程的读写、写操作时;
  在每次调用容器的成员函数期间都要锁定该容器;
  在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
  在每个在容器上调用的算法执行期间锁定该容器

lambda表达式

  大神链接

  [capture] (params)opt->ret{body;};
  [函数对象参数] (操作符重载函数参数)mutable或exception声明->返回值{函数体};
1.函数对象参数

操作符意义
[]不截取任何变量
[&]截取外部作用域的所有变量,并且按引用传递
[=]截取外部作用域的所有变量,并且按值传递
[=,&foo]截取外部作用域的所有变量,并且按值传递,但是对foo变量使用引用
[bar]截取bar变量,并且按值传递,同时不截取其他变量
[this]截取当前类中的this指针,如果使用&或者=就默认添加此选项

2.重载参数列表
  调用的时候需要传递的参数(参数可以缺省)
3.返回类型(可以自动推导,可以省略)
4.函数体
5.函数选项opt

选项意义
mutable表示函数体可以修改捕获的变量,同时可以访问捕获对象的非常属性的成员函数
exception是否抛出异常以及抛出何种异常
attribute用来声明属性

lambda表达式如何实现

大神链接
  编译器为我们创建了一个匿名类,重载了() (仿函数机制),捕获的变量相当于将该变量定义在该类中的成员变量中。

什么样的函数可以抛出异常?构造函数可不可以抛出异常?

大神链接

析构函数:
  除析构函数,别的都可以抛出异常。
  1.不要在析构函数中抛出异常!虽然C++并不禁止析构函数抛出异常,但这样会导致程序过早结束或出现不明确的行为。
  2.如果某个操作可能会抛出异常,class应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。
  3.如果析构函数中异常非抛不可,那就用try catch来将异常吞下,但这样方法并不好,我们提倡有错早些报出来。

构造函数:
  1. 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
  2. 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
  3. 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。

最后总结如下:

  1. 构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!
  2. 不要在析构函数中抛出异常!

如何限制对象只能在栈上?如何限制对象只能在堆上?

大神链接
只在堆上:将析构函数私有化
只占栈上:将new和delete重载并私有化

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值