C++基础面试总结(一)

C++基础面试总结(一):

1. C和C++的区别

  • C是面向过程的语言,C++是面向对象的语言;C++三大特性。
    继承(提高代码复用性)、封装(隐藏对象属性和细节,对外提供访问方式)、多态(提供接口,扩展)
  • 动态内存管理的方法不同。C是使用malloc/free库函数,而C++除此之外还可以使用new/delete关键字。
  • C中的结构体struct和C++的类的区别。C++的类独有的,类的相关知识点(巴拉巴拉),C没有,但C中的struct可以在C++中正常使用,且C++对struct进行了扩展。
  • C++支持函数重载,C不支持函数重载。
  • C++中有引用,而C没有。可引出:引用和指针的区别。

1+.面向过程和面向对象的区别

  面向对象就是高度实物抽象化、面向过程就是自顶向下的编程
  1. 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用。

  2. 面向对象是把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

  3. 面向过程的优缺点

    	优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
    	缺点:没有面向对象易维护、易复用、易扩展
    
  4. 面向对象的优缺点

       优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护 
       缺点:性能比面向过程低
    

2+. 面向对象三大特性

  1. 继承(提高代码复用性)

  2. 封装(隐藏对象属性和细节,对外提供访问方式)

    封装可以隐藏实现细节,使得代码模块化。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。在面向对象编程上可理解为:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  3. 多态(提供接口,扩展)

    在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
    如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
    

    C++中,实现多态有以下方法:虚函数,抽象类,覆盖,模板,条件是要有重写,要有继承,父类指向子类。

    1. 静态多态(重载,模板)
      是在编译时,就确定调用函数的类型。
    2. 动态多态(覆盖,虚函数)
      在运行时,才确定调用的是哪个函数。运行基类指针指向派生类的对象,并调用派生类的函数。
2++1.构造函数能否是虚函数

不能。原因如下:
1). 构造一个对象时,必须知道对象实际类型,而虚函数是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功,编译器就无法知道对象的实际类型,是该类本身,还是派生类,还是其他。

2). 虚函数的执行依赖于虚函数表,而虚函数表是在构造函数中进行初始化的,即初始化虚表指针(vptr),使得正确指向虚函数表。而在构造对象期间,虚函数表(vtable)还没有被初始化,将无法进行。

2++2.为什么析构函数声明为虚函数

1)首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。
2)如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

2++3.析构函数和构造函数可以抛出异常吗?

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

析构函数
1). 不要在析构函数中抛出异常!虽然C++并不禁止析构函数抛出异常,但这样会导致程序过早结束或出现不明确的行为。
2). 如果某个操作可能会抛出异常,类应提供一个普通函数(而非析构函数),来执行该操作。目的是给客户一个处理错误的机会。

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

虚函数的作用: 虚函数实现了多态的机制。基类定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,

实现原理:

当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的虚函数表(虚函数表实际上就是一个函数指针数组,有的编译器用的是链表)。虚函数表数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个虚函数表指针,虚函数表指针指向该虚函数表的地址。所以当一个类有虚函数的,是占用内存的,占用一个指针大小的内存。

虚函数表存放规则:

  1. 虚函数指针按照其声明顺序放于虚函数表中。
  2. 如果子类覆盖了父类的虚函数,将覆盖虚函数表中原来父类虚函数的位置。
  3. 如果派生类有多个父类,子类的成员函数存放在第一个父类的表中。

虚函数表存放位置
C++虚函数存储在只读数据段(.rodata),也就是C++内存模型中的常量存储区。

虚函数表指针存放位置

一个类存在虚函数,那么编译器就会为这个类生成一个虚表,在虚表里存放的是这个类所有虚函数的地址。当生成类对象的时候,编译器会自动的将类对象的前四个字节设置为虚表的地址,而这四个字节就可以看作是一个指向虚表的指针。虚表里依次存放的是虚函数的地址,每个虚函数的地址占4个字节。

2++5.为什么要有纯虚函数
  • 定义纯虚函数,可以将类升级位为抽象类,抽象类不能进行实例化,并且必须在子类中实现

  • 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

     纯虚函数要求在派生类中 必须予以重写以实现多态性,同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
    
2++6.构造函数、析构函数、拷贝构造函数、移动构造函数、重载= (五大件)
  • 构造函数:初始化对象的内存空间。可以重载、没有返回值、函数名字和类名字一样

  • 析构函数:释放对象所占资源。不能重载、没有返回值、~类名字

  • 拷贝构造函数:拿一个已存在的对象来初始化一个相同类型的新对象。

       定义:classname (const classname &obj)
    
  • 移动构造函数: 用一个对象的已有指针move到新的一个指针。

       定义:classname(const classname &&obj) -->可引出右值引用
    
例:string类

class string{
	private:
		char* m_data;
	public:
		//string(){};
		//将有参构造和无参构造合为此一个函数
		string(const char *str = nullptr){
			   if (str == nullptr) {
				m_data = new char[1];
				*m_data = '\0';
				cout << "Default constructor" << endl;
			}
			else {
				int length = strlen(str);
				m_data = new char[length + 1];
				strcpy(m_data, str);
				cout << "Pass argument constructor" << endl;
		}
		//拷贝构造函数,需处理好本对象的动态内存空间的分配,以及另一个对象的数据的拷贝
		string(const string & other){
			int len=strlen(other.m_data);
			m_data=new char[len+1];
			strcpy(m_data,other.m_data);
		}
		//我们是不是要析构本对象的数据? 
	    //必须要析构本对象的数据,不然赋值过来,原来的动态内存空间就是野空间了,这就是内存泄露! 
		string &operator=(const string &other){
			if(this!=&other){
				if(!m_data) delete[] m_data;
				int len=strlen(other.m_data);
				m_data=new char[len+1];
				strcpy(m_data,other.m_data);
			}
			return *this;
		} 
		//不需要分配内存空间。需要将other.m_data置为空
		string(const string && other){
			m_data = other.m_data;
			other.m_data =nullptr;
		}
		~string(){ delete[] m_data;};
}

3+. struct和class的区别

  • C中的strcut不能有函数,但C++中可以。C++中的struct对C中的struct进行了扩充,它已经不再只是一个包含不同数据类型的数据结构了。

  • 默认的继承访问权限. struct是public的,class是private的

  • struct B : struct A就是为了指明是public继承,而不是用默认的private继承,若class B :
    A则是private继承。

4+.重载和重写

Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
条件:(1)相同的范围(在同一个类中)(2)函数名字相同(3)参数或返回值不同;

Override(重写):是指派生类函数覆盖基类函数。
条件:(1)不同的范围(分别位于派生类与基类)(2)函数名字相同(3)参数或返回值相同;

****补充:[重载的底层原理]

*C语言没有重载: 在C语言中被解析为_Add,三个一样,所以不能进行区分,因此C语言不支持函数重载

    "int __cdecl Add(int,int)"                (?Add@@YAHHH@Z)
	"double __cdecl Add(double,double)"       (?Add@@YAHHH@Z)
	"long __cdecl Add(long,long)"             (?Add@@YAHHH@Z)
  • C++重载: 底层的重命名机制将Add函数根据参数的个数,参数的类型,返回值的类型都做了重新命名。
    那么借助函数重载,一个函数就有多种命名机制。 _Add_int_int,_Add_long_long,_Add_double_double
    "int __cdecl Add(int,int)"                (?Add@@YAHHH@Z)
	"double __cdecl Add(double,double)"       (?Add@@YANNN@Z)
	"long __cdecl Add(long,long)"             (?Add@@YAJJJ@Z)

2.const、static、extern、explicit、volatile关键字

const关键字(不可修改)

修饰变量,说明该变量不可以被改变,在声明的时候必须初始化。
修饰参数 是为了防止函数体内可能会修改参数原始对象。

 char * const cp;  //指针常量;指针是常量
 const char * cp1;
 char  const* cp2; //常量指针;常量的指针,值是指针

static关键字(对外不可见)

  1. 修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。

  2. 修饰局部变量时,表明该变量的值不会因为函数终止而丢失。

  3. 修饰函数时,表明该函数只在同一文件中调用。

  4. 修饰类的数据成员,表明对该类所有对象这个数据成员都只有一个实例。即该实例归 所有对象共有。

  5. 用static修饰不能访问非静态数据成员的类成员函数。这意味着一个静态成员函数只能访问它的参数、类的静态数据成员和全局变量

    1、2、3C和C++共有,4、5C++独有

为什么静态函数只能调用静态变量?

因为静态是针对类的,而成员变量为对象所有。

静态成员函数不属于任何一个类对象,非静态成员随类对象的产生而产生,所以静态成员函数"看不见"非静态成员,自然也就不能访问了

类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。

在一个类的静态成员中去访问其非静态成员之所以会出错是因为在类的非静态成员不存在的时候类的静态成员就已经存在了,访问一个内存中不存在的东西当然会出错。

extern关键字

extern关键字主要修饰变量或函数,表示该函数可以跨文件访问,或者表明该变量在其他文件定义,在此处引用。

补充[extern“C”作用]
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

explicit关键字(防止隐式转化)

 常用于修饰一个参数的构造函数,用来防止构造函数定义的隐式转换。
	class string{
		public:
			explicit string(int n){};
			string(const char* p){};
	}
	string s1=10; //不加explicit关键字时,可以编译通过,因为可以做隐式类型转换。加了后,禁止类型的转换,编译出错。
	string s2(10);//正常的定义(显式转换),编译都可通过。

volatile关键字(避免编译器指令优化)

volatile关键字是一种类型修饰符,用它声明的变量表示可以被某些编译器未知的因素更改,编译器不再对有此变量的代码进行优化。其他的代码使用volatile修饰的变量的时候,总是重新从该变量的内存读取数据,从而保证对特殊地址的稳定访问。

	void main(){
		int i=10;
		int a=i;
		cout<<a;
		__asm{
			mov dword ptr [ebp-4] ,20h //此语句作用:改变内存中i的值(编译器不知道)
		}
		int b=i;
		cout<<b;
	}

	//此函数在debug的时候输出 10,32
	//在release时候输出 10,10 (内存的改变没有通过编译器,b在赋值的时候用的cache中的值)
			
	void main(){
		volatile int i=10; //加上volatile修饰后。debug和release时,输出都是10 ,32。因为volatile保证每次用变量i的时候都是从内存中读取数据。
		int a=i;
		cout<<a;
		__asm{
			mov dword ptr [ebp-4] ,20h //此语句作用:改变内存中i的值(编译器不知道)
		}
		int b=i;
		cout<<b;
	}
	//此函数在debug的时候输出 10,32
	//在release时候输出 10,32

volatile常用的地方

中断服务程序中修改的供其他程序使用的变量需要加volatile
多任务时各任务间共享的标志

3.引用与指针

  1. 内存分配上:引用在定义的时候必须进行初始化(引用与某个对象绑定后就不再改变),并且不能够改变。指针在定义的时候不一定要初始化,可为空,并且指向的空间可变。
  2. 级数:有多级指针,但是没有多级引用,引用只有一级。
  3. 自增含义:指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
  4. Sizeof大小:Sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。
  5. 访问方式:引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
  6. 引用底层是通过指针实现。使用指针前最好做类型检查,防止野指针的出现。引用可避免野指针出现。

4.#include<file.h> 与 #include "file.h"的区别?

前者是从标准库路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h

5. 编译性语言和解释性语言的本质区别和优缺点

根本区别

  1. 计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。两种方式只是翻译的时间不同
  2. 解释性语言不用编译,在运行时翻译
  3. 编译性语言是编译的时候直接编译成机器可以执行的语言,编译和运行是分开的,但是不能跨平台。

编译性语言的优缺点

  • 优点 : 运行速度快,代码效率高,编译后程序不可以修改,保密性好
  • 缺点 : 代码需要经过编译方可运行,可移植性差,只能在兼容的操作系统上运行。

解释性语言的优缺点

  • 优点 :可移植性好,只要有解释环境,可以在不同的操作系统上运行。
  • 缺点 : 运行需要解释环境,运行起来比编译的要慢,占用的资源也要多一些,代码效率低,代码修改后就可以运行,不需要编译过程

6.GCC编译流程

1). 预处理阶段:hello.c – “gcc -E,头文件展开,宏替换,删除注释、空白” --> hello.i
2). 编译阶段:hello.i – “gcc -s, 检查语法规范、生成汇编文件” --> hello.s
3). 汇编阶段:hello.s – “gcc -c, 生成二进制文件” --> hello.o
4). 链接阶段:hello.o – "链接库文件–>可执行文件

7.指针函数与函数指针

指针函数int* f(int x, int y)本质是函数,返回值为指针;
函数指针int (*f)(int x)本质是指针,指向函数的指针。

void test001(){
	printf("hello, world");
}
int main(){
	void(*myfunc)() = test001;//将函数写成函数指针
	myfunc(); //调用函数指针 hello world
}

/*
1. test001的函数名与myfunc函数指针都是一样的,即都是函数指针。
 test001函数名是一个函数指针常量,而myfunc是一个函数指针变量,这是它们的关系。
2. 函数指针多用于回调函数,回调函数最大的优势在于灵活操作,可以实现用户定制的函数,降低耦合性,实现多样性。
3. */

8.C++中一个空类的大小为什么是1?

空类,编译器能自动生成的成员函数(只有用到了,编译时才会生成):默认构造、析构、拷贝构造、赋值运算符、两个取址运算符(一个const,一个非const)

class test{};
	test();
	~test();
	test(const test& other);
	test& operator=(const test& other);
	test* operator&();
	const test* operator() const;// 第一个const说明返回值时const类型的,第二个const说明此函数是常成员函数,不会改变类的成员变量。

实例化的原因(空类同样可以被实例化),每个实例在内存中需要有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节

9.大端存储和小端存储

大端(存储)模式:一个数据的低字节内容存放在高地址中,高字节的内容存放在低地址中

  • (简单的说就是:低字节,高地址。高字节,低地址。----->大端)

小端(存储)模式:一个数据的低字节内容存放在低地址中,高字节的内容存放在高地址中

  • (简单的说就是:低字节,低地址。高字节,高地址。----->小端)

10.sizeof()和strlen()区别

sizeof()是C的一个运算符,作用是求变量或者数据类型占用的内存字节数,计算的是实际分配的内存空间大小,不受内部存储内容影响。

strlen()是函数,作用是返回字符串的长度。完成的功能是从对用字符串的第一个地址开始遍历,直到遇到结束符‘\0’。返回的长度大小不包括结束符号, 函数的参数必须是字符型指针char*。

//例1:
char str[20] ="0123456789";
strlen(str); //10;
sizeof(str);//20 ,因为申请的时候申请的内存是20个char。

//例2:
char a[]="0123456789";
strlen(a); //10
sizeof(a); //11 因为在结束的位置有一个结束符 ‘\0’

补充. sizeof

(1) 求对象或者类型的大小,单位为字节B

(2) sizeof能求得静态分配内存的数组的长度

int a[10];int n = sizeof(a);  //假设sizeof(int)等于4,则n= 10*4=40;
char ch[]=”abc”;sizeof(ch);   //结果为4,注意字符串数组末尾有’\0’!

通常我们可以利用sizeof来计算数组中包含的元素个数,其做法是:int n = sizeof(a)/sizeof(a[0]);

非常需要注意的是对函数的形参数组使用sizeof的情况。举例来说,假设有如下的函数:

	void fun(int array[10])
	{
		int n = sizeof(array);//这里n等于4,事实上,不管形参是int型数组,还是float型数组,或者其他任何用户自定义类型的数组,也不管数组包含多少个元素,这里的n都是4!**原因是在函数参数传递时,数组被转化成指针了。**
	}

函数参数传递时,数组被转化成指针的原因
直接传递整个数组的话,那么必然涉及到数组元素的拷贝(实参到形参的拷贝),当数组非常大时,这会导致函数执行效率极低,而只传递数组的地址(即指针)那么只需要拷贝4byte。

(3)sizeof不能求得动态分配的内存的大小!

	//例1:
    int *a = new int[10]; int n = sizeof(a);  //n=4,因为a是指针。
	//例2:
	int fun(int& num,const int& inc)
	{
		float div = 2.0; double ret =0;
		num = num+inc;  ret = num/div;
		return ret;
	}
	int a = 3; int b = 5; 
	cout<<sizeof(fun(a,b)); //求得是返回值类型的小大,即int ,大小为4
	cout<<a<<endl;          //sizeof中的函数并没有执行,a的值仍然是3,没有修改

11. 函数参数在栈中的先后顺序是怎么样的呢?(这里解释栈的增长方向是向内存减少的方向增加)

void func(int k,char * sz,int b)
{
 return;
}

我们要知道两点:
第一,参数压栈的顺序是从右到左,对应到上面的代码也就是先压b,然后是sz,最后是k。
第二,栈空间是从高地址向低地址发展的。
所以,我们可以估计出他们在内存中的分布,从高地址到低地址的顺序是:b, sz,k。 同时由于三个参数都占4个字节大小,所以两个参数的首地址之间相隔四个字节。

12. 结构体大小计算规则

(1). 结构体的大小等于结构体内最大成员大小的整数倍;
(2). 结构体内的成员的首地址相对于结构体首地址的偏移量是其类型大小的整数倍,比如说double型成员相对于结构体的首地址的地址偏移量应该是8的倍数;
(3). 为了满足规则1和2编译器会在结构体成员之后进行字节填充!

	struct A{
			 int num1;
			 int num2;                   // sizeof(A) :16
			 double num3;
	};
	struct B{
			 int num1;
			 double num3;                // sizeof(B) :24
			 int num2;
	};

13. 联合体大小计算

结构体在内存组织上是顺序式的,联合体则是重叠式,各成员共享一段内存,所以整个联合体的sizeof也就是每个成员sizeof的最大值

14. 静态编译与动态编译的区别

动态编译:

动态编译的可执行文件需要附带一个的动态链接库,在执行时,需要调用其对应动态链接库中的命令。

优点: 缩小执行文件本身的体积,加快了编译速度,节省了 系统资源。

缺点: 一是哪怕是很简单的程序,只用到了链接库中的一两条命令,也需要附带一个相对庞大的链接库;
  二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

静态编译

静态编译就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应动态链接库(.so)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。所以其优缺点与动态编译的可执行文件正好互补。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值