C++ 整理

C++一直在用,但是类中有很多知识一直没怎么用到,如今秋招在即,抽时间整理一下吧~

https://www.cnblogs.com/yasanlun/p/3838531.html

目录

1、C++特点

1.1 class和struct

 C++的 class和struct的区别

c语言中的struct 和 c++ 的struct的区别

1.2 友元函数:

1.3 重载

1.4 类,派生类的创建顺序与析构顺序

1.5 友元函数和友元类

2、程序设计注意事项

2.1 char*和char[]

2.2 strlen和sizeof

2.3 #define的直接替换

2.4 ##和#

2.5 int* ,int&和int* &

2.6 大端,小端

2.7 什么时候调用拷贝构造函数

2.8 实参和形参

2.9 数组的内存排列

2.10 二维数组作为函数入参

2.11 动态数组

2.12 C和C++的区别

2.13 C++中指针和引用的区别

2.14 结构体struct和共同体union(联合)的区别

2.15 #define和const的区别

2.16 重载overload,覆盖override,重写overwrite,这三者之间的区别

2.17  new、delete、malloc、free之间的关系

2.18 delete和delete[]的区别

2.19 STL库用过吗?常见的STL容器有哪些?算法用过几个?

2.20 const知道吗?解释一下其作用

2.21 虚函数是怎么实现的

2.22 堆和栈的区别

2.23 关键字static的作用

2.24 STL中map和set的原理(关联式容器)

2.25 、#include #include "file.h" 的区别

2.26 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?

2.27 定义和声明的区别

2.28 C++文件编译与执行的四个阶段

2.29 STL中的vector的实现,是怎么扩容的?

2.30 STL中unordered_map和map的区别

2.31 C++的内存管理

2.32 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

2、析构函数最好声明为虚函数

2.33 静态绑定和动态绑定的介绍

1)对象的静态类型和动态类型

2)静态绑定和动态绑定

2.34 引用是否能实现动态绑定,为什么引用可以实现

2.35 深拷贝和浅拷贝的区别

2.36 什么情况下会调用拷贝构造函数(三种情况) 

2.37 C++的四种强制转换 

2.38 调试程序的方法 

2.39 extern“C”作用

2.40 typdef和define区别

2.41 volatile关键字在程序设计中有什么作用

2.42 引用作为函数参数以及返回值的好处

2.43 纯虚函数

2.44 什么是野指针

2.45 线程安全和线程不安全

2.46 C++中内存泄漏的几种情况

2.47 栈溢出的原因以及解决方法

2.48 C++ 11有哪些新特性

2.49 C++中vector和list的区别

2.50 C语言的函数调用过程

2.51 C++中的基本数据类型及派生类型

2.52 C++线程中的几种锁机制

2.53 string的c_str()和strcpy的差别

3 虚函数

3.1 虚函数表详解

3.2 关于虚函数与纯虚函数

4 C++面试题:

4.1 类指针为NULL时,仍可以访问类函数

4.2 c库和系统调用的关系

4.3  int(*a[10])(int)



1、C++特点

C++三大特点:封装、继承、多态;

其中多态又分为:

  • 静态多态:包括 函数重载 和 泛型编程
  • 动态多态:虚函数

1.1 class和struct

 C++的 class和struct的区别

  • C++的struct,默认为public时;
  • class默认是private

c语言中的struct 和 c++ 的struct的区别

C中struct只是一个自定义的数据类型(结构体),struct是抽象的数据类型,支持一些类的操作和定义

1.2 友元函数:

友元函数并不是类成员,但须在类中声明,并且加前缀friend

友元函数可以访问private变量,但是不能被继承(本质上友元函数不属于类成员,只是开了个后门)

代码示例:

#include <iostream>
#include <string>
#include <sstream>
using namespace std;
class test {
public:
	test(string s1, int t, string s2) :name(s1),age(t),job(s2){}//构造函数
	test() = default;//默认构造函数
	int add_age(int n);//定义在外部的类操作函数
	friend void show_data(int flag, test &a);//友元函数
	friend std::ostream &operator<<(std::ostream& os, test &a) {
		os << "name" << a.name << endl; os << "age:" << a.age << endl; os << "job:" << a.job << endl;
		return os;//注意 return os不能忘记
	}
private:
	string name;
	int age;
	string job;
protected:

};
//定义在外部的类操作函数
int test::add_age(int n)
{
	age = n;
	return 0;
}
//友元函数
void show_data(int flag, test &a)
{
	cout << a.name << endl;
	cout << a.age << endl;
	cout << a.job << endl;
}
int main()
{
	test a("jk", 25, "student");
	cout << a << endl;//函数重载
	a.add_age(30);//更改年龄
	cout << "更改年龄:" << endl;
	cout << a << endl;
	cout << "友元函数:" << endl;
	show_data(1,a);//友元函数
	getchar();
}

注意:

友元函数可以领其他类或函数访问类的非公有成员;

 

1.3 重载

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

函数重载

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。

int fun(int a,char b);

int fun(int a,double b);

void fun(int a,char b); //返回类型不同,不属于重载

运算符重载:

#include <iostream>
#include <sstream>
using namespace std;
class A{
public:
    //构造函数的重载
    A()=default;
    A(int x_,int y_):x(x_),y(y_){}
    A(int x_,int y_,int z_){
        x=x_*z_;
        y=y_*z_;
    }
    //操作符重载
    A operator+(const A b){
        A a;
        a.x=this->x+b.x;
        a.y=this->y+b.y;
        return a;
    }
    void print(){
        cout<<x<<","<<y<<endl;
    }
private:
    int x;
    int y;
};

int main()
{
    A a(1,2);
    A b(2,3);
    a=a+b;
    a.print();
    getchar();
}

1.4 类,派生类的创建顺序与析构顺序

#include <iostream>
#include <sstream>
using namespace std;
class A
{
public:
	A() { printf("A,"); }
	virtual ~A() { printf("A,"); }
};
class AA
{
public:
	AA() { printf("AA,"); }
	virtual ~AA() { printf("AA,"); }
};
class B :public A
{
public :
	B() :m_AA() { printf("B,"); }
	virtual ~B() { printf("B,"); }
private:
	AA m_AA;
};

int main()
{
	B *pB = new B();
	delete pB;
	getchar();
}

输出:A,AA,B,B,AA,A,   

  1. 要构造B类,第一步是构造B类的父类A,输出A;
  2. 第二步构造B类中定义的AA,输出AA,
  3. 最后再执行B类的默认构造函数B(),输出B;
  4. 而析构函数则刚好相反。。。

解释:

C++构造函数调用顺序

  1. 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
  2. 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)
  3. 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
  4. 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
  5. 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数,否则两个类的实现变成紧耦合的(tightly coupled)将更加难于正确地修改或扩展基类的实现。(基类设计者的责任是提供一组适当的基类构造函数)

析构函数顺序则刚好相反!

 

1.5 友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。

通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

有元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。

friend 类型 函数名(形式参数);

一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。        

 friend class 类名; 

使用友元类时注意: 

(1) 友元关系不能被继承。 
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。 
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

 

2、程序设计注意事项

2.1 char*和char[]

考察内存分配的理解,char*指针指向内存,而char[]需要分配内存,const char[]亦是如此,不要被迷惑。

#include <iostream>
#include <sstream>
using namespace std;
int main()
{
	char *s1 = "hello world";//s1是一个指针,存放字符串地址
	char s2[] = "hello world";//s2本身就是一个数组的地址了
	cout << s1 << endl;
	cout << s2 << endl;
	s1 = "w";
    //*s1="w";//不能这么做,因为*s1原本指向的是字符串常量
	*s2 = 's';
    //s2='s';//不能这么做,s2是数组的首地址,应该通过*s2来改变数组的值
	cout << s1 << endl;
	cout << s2 << endl;
	getchar();
}

分析:char *s1,s1是一个指针,存放的是地址,因此可以通过s1="w",改变s1的地址,不能使用*s1="w";而s2是数组首地址,因此通过*s2改变数组的值

 

2.2 strlen和sizeof

#include <iostream>
#include <sstream>
using namespace std;
int main()
{
	char s[] = "hello world";
	cout << "strlen(s):" << strlen(s) << endl;
	cout << "sizeof(s):"<<sizeof(s) << endl;
	getchar();
}

“hello world”其实是"hello world\0",而strlen自动忽略'\0',因此长度是11bytes,但是字符串数组中仍存储"\0",因此是12bytes;

 

2.3 #define的直接替换

#include <iostream>
#include <sstream>
#define fun(x) x*x
using namespace std;
int main()
{
	cout << fun(fun(2+2*2))*2<< endl;
	getchar();
}

一步一步展开不要心急!

  1. fun(fun(2+2*2))*2
  2. fun(2+2*2)*fun(2+2*2)*2
  3. 2+2*2*2+2*2*2+2*2*2+2*2*2
  4. 结果34

 

2.4 ##和#

#include <iostream>
#include <sstream>
#define PRINT(id) printf("var"#id"=%d\n", var##id);
using namespace std;
int main()
{
	int var1 = 15;
	PRINT(1);
	getchar();
}

输出:(注意#id”   var##id)

var1=15

 

2.5 int* ,int&和int* &

#include <iostream>
#include <sstream>
using namespace std;
void fun(int* &i)
{
	i++;
}

int main()
{
	int a = 9;
	int *i=&a;
	fun(i);
	cout << i << endl;
	getchar();
}

相当于输入一个指针i,但是是引用,因此函数内对i的操作会影响main中的i,因此输出的其实是原地址 i+1,00BAFD04  

 

2.6 大端,小端

在小端机器上运行以下函数:

#include <iostream>
#include <sstream>
using namespace std;
int main()
{
	int i = 1024;
	char *p = (char*)&i;
	for (int i = 0; i < 4; i++)
		printf("%02x,", p[i]);
	getchar();
}

解析:因为小端的函数是数据的低位存在地址的低位,int型的1024=2^8,相当于(地址从大到小)0000 0000 1000 0000,因此函数输出从地址低位到地址高位输出,分别是0x00,0x40,0x00,0x00

 

2.7 什么时候调用拷贝构造函数

(1)用类的一个对象去初始化另一个对象时

(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用

(3)当函数的返回值是类的对象或引用时

#include <iostream>
using namespace std;
 
class A
{
private:
	int a;
public:
	A(int i){a=i;}	//内联的构造函数
	A(A	&aa);
	int geta(){return a;}
};
 
A::A(A &aa)		//拷贝构造函数
{
	a=aa.a;
	cout<<"拷贝构造函数执行!"<<endl;
}
 
int get_a(A aa)		//参数是对象,是值传递,会调用拷贝构造函数
{
	return aa.geta();
}
 
int get_a_1(A &aa)	//如果参数是引用类型,本身就是引用传递,所以不会调用拷贝构造函数
{
	return aa.geta();
}
 
A get_A()		//返回值是对象类型,会调用拷贝构造函数。会调用拷贝构造函数,因为函数体内生成的对象aa是临时的,离开这个函数就消失了。所有会调用拷贝构造函数复制一份。
{
	A aa(1);
	return aa;
}
 
A& get_A_1()	//会调用拷贝构造函数,因为函数体内生成的对象aa是临时的,离开这个函数就消失了。所有会调用拷贝构造函数复制一份。
{
	A aa(1);
	return aa;
}
 
int _tmain(int argc, _TCHAR* argv[])
{
	A a1(1);
	A b1(a1);			//用a1初始化b1,调用拷贝构造函数
	A c1=a1;			//用a1初始化c1,调用拷贝构造函数
 
	int i=get_a(a1);		//函数形参是类的对象,调用拷贝构造函数
	int j=get_a_1(a1);		//函数形参类型是引用,不调用拷贝构造函数
 
	A d1=get_A();		//调用拷贝构造函数
	A e1=get_A_1();		//调用拷贝构造函数
 
	return 0;
}

 

2.8 实参和形参

实参(argument):

  全称为"实际参数"是在调用时传递给函数的参数. 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值。      

形参(parameter):

全称为"形式参数" 由于它不是实际存在变量,所以又称虚拟变量。是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数.在调用函数时,实参将赋值给形参。因而,必须注意实参的个数,类型应与形参一一对应,并且在调用时,实参必须要有确定的值。

因为形参只能是变量,实参可以是常量,变量,表达式

2.9 数组的内存排列

 

2.10 二维数组作为函数入参

c、c+中,二维数组在内存分配上其实是一维的。同时要访问数组元素,一般是*(int*)这种形式访问的,因此需要把int** a强制转化为int* a,后面再加上偏移量。

#include <iostream>
using namespace std;
void deal(int** a, int m, int n) {
	for (int i = 0; i < m; i++) {
		for (int j = 0; j < n; j++) {
			*((int*)a + i*m + j) = 1;
		}
	}
}

int main() {

	int a[5][5];
	deal((int**)a, 5, 5);
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 5; j++) {
			cout << a[i][j] << " ";
		}
		cout << endl;
	}
	getchar();
}

 

2.11 动态数组

int main() {
	int n, m;
	cin >> n >> m;
	int **a;
	a = (int**)malloc(sizeof(int*)*n);
	for (int i = 0; i < m; i++) {
		a[i] = (int*)malloc(sizeof(int)*m);
	}

	//偷懒一下,就不初始化了
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++)
			cout << a[i][j] << " ";
		cout << endl;

	}
	while (1);
}

 

2.12 C和C++的区别

1)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。

#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
class A{
public:
  int a; 
  virtual void fun(int val){a=val;cout<<a<<endl;}
};
class B:public  A//继承
{
  int b;
  virtual void fun(int val){int b=100*val;cout<<b<<endl;}//多态,同一个接口调用不同函数
};
int main(int argc, char **argv) {
  A *a;
  B b;
  a=&b;
  a->fun(1);//多态
  cout<<"hello"<<endl;
  while(1);
  return 0;
}

2)C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。

3)C++支持函数重载,C不支持函数重载

4)C++中有引用,C中不存在引用的概念

 

2.13 C++中指针和引用的区别

1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;

引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的

2)引用只有一级,而指针可以有多级

3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作

引用传参的时候,传进来的就是变量本身,因此变量可以被修改

 

2.14 结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型

共同体:不同类型的几个变量共同占用一段内存

1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;

共同体中的所有成员占用同一段内存,它们不能同时存在;

2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、

结构体为什么要内存对齐呢?

 

2.15 #define和const的区别

1)#define定义的常量没有类型,所给出的是一个立即数,在汇编的时候就被替换const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数const不可以定义函数

 

2.16 重载overload,覆盖override,重写overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数和返回值不同,这就是函数重载

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

 

2.17  new、delete、malloc、free之间的关系

new/delete,malloc/free都是动态分配内存的方式

1)malloc对开辟的空间大小严格指定,而new只需要对象名

2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

既然有了malloc/free,C++中为什么还需要new/delete呢?

因为malloc/free是库函数而不是运算符,不能把执行构造函数和析构函数的功能强加于malloc/free

 

2.18 delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

 

2.19 STL库用过吗?常见的STL容器有哪些?算法用过几个?

STL包括两部分内容:容器和算法

容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器

序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist

关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set

算法有排序,复制等,以及各个容器特定的算法

迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

 

2.20 const知道吗?解释一下其作用

const修饰类的成员变量,表示常量不可能被修改

const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

 

2.21 虚函数是怎么实现的

每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),

类的示例对象不包含虚函数表,只有虚指针;

派生类会生成一个兼容基类的虚函数表。

 

2.22 堆和栈的区别

1)栈 stack 存放函数的参数值、局部变量,由编译器自动分配释放

堆heap,是由new分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收

2)因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片

3)堆的生长空间向上,地址越大,栈的生长空间向下,地址越小

 

2.23 关键字static的作用

1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

static变量可以被修改,函数中定义的static在函数推出后也不会被释放

 

2.24 STL中map和set的原理(关联式容器)

map和set的底层实现主要通过红黑树来实现

红黑树是一种特殊的二叉查找树

1)每个节点或者是黑色,或者是红色 

2)根节点是黑色

3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

4)如果一个节点是红色的,则它的子节点必须是黑色的

5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。

 

2.25 、#include<file.h> #include "file.h" 的区别

前者是从标准库路径寻找
后者是从当前工作路径

 

2.26 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

方法:malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界

 

2.27 定义和声明的区别

声明是告诉编译器变量的类型和名字,不会为变量分配空间

定义需要分配空间,同一个变量可以被声明多次,但是只能被定义一次

 

2.28 C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容

2)编译:编译成汇编代码

3)汇编:把汇编代码翻译成目标机器指令

4)链接:链接目标代码生成可执行程序

 

2.29 STL中的vector的实现,是怎么扩容的?

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。 
vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。

vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

 

2.30 STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

2.31 C++的内存管理

在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区

  • 栈:存放函数的参数和局部变量,编译器自动分配和释放
  • 堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收
  • 自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放
  • 全局/静态存储区:存放全局变量和静态变量
  • 常量区:存放常量,不允许被修改

 

2.32 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

 

2.33 静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

1)对象的静态类型和动态类型

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

动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改

2)静态绑定和动态绑定

静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

只有虚函数才使用的是动态绑定,其他的全部是静态绑定

 

2.34 引用是否能实现动态绑定,为什么引用可以实现

可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。

 

2.35 深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

 

2.36 什么情况下会调用拷贝构造函数(三种情况) 

系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)

生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数

调用拷贝构造函数的情形:

1)用类的一个对象去初始化另一个对象的时候

2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用

3)当函数的返回值是类的对象或者引用的时候

举例:

#include <iostream>
#include <string>

using namespace std;

class A{
    private:
        int data;
    public:
        A(int i){ data = i;}     //自定义的构造函数
        A(A && a);              //拷贝构造函数 
        int getdata(){return data;} 
};
//拷贝构造函数 
A::A(A && a){
    data = a.data;
    cout <<"拷贝构造函数执行完毕"<<endl;
}
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
    return a.getdata();
}
//参数是引用,引用传递,不调用拷贝构造函数 
int getdata2(A &a){
    return a.getdata();
} 
//返回值是对象类型,会调用拷贝构造函数
 A getA1(){
     A a(0);
     return a;
 } 
 //返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
 A& getA2(){
     A a(0);
     return a;
 } 
 
 int main(){
    A a1(1);  
    A b1(a1);                   //用a1初始化b1,调用拷贝构造函数  
    A c1=a1;                    //用a1初始化c1,调用拷贝构造函数  
  
    int i=getdata1(a1);            //函数形参是类的对象,调用拷贝构造函数  
    int j=getdata2(a1);          //函数形参类型是引用,不调用拷贝构造函数  
  
    A d1=getA1();               //调用拷贝构造函数  
    A e1=getA2();                 //调用拷贝构造函数  
  
    return 0;  
}  

 

2.37 C++的四种强制转换 

类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)

(new-type) expression
new-type (expression)
隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:

static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :编译时期的静态类型检查

static_cast < type-id > ( expression )

该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的

2)dynamic_cast:运行时的检查

用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用

dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。

dynamic_cast如果不能转换返回NULL

源类中必须要有虚函数,保证多态,才能使用dynamic_cast<source>(expression)

3)const_cast

去除const常量属性,使其可以修改 ; volatile属性的转换

4)reinterpret_cast

通常为了将一种数据类型转换成另一种数据类型

 

2.38 调试程序的方法 

windows下直接使用vs的debug功能

linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符、

 

2.39 extern“C”作用

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

2.40 typdef和define区别

#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查

typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

typedef    (int*)      pINT;

#define    pINT2   int*

效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

 

2.41 volatile关键字在程序设计中有什么作用

参考:https://www.xuebuyuan.com/3257030.html

volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。

volatile告诉编译器不要优化,每次对volatile修饰的变量取用时,都去其地址所在处去读取。

 

2.42 引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(所以没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

2)不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。 

 

2.43 纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承

包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

普通函数是静态编译的,没有运行时多态

 

2.44 什么是野指针

野指针不是NULL指针是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存

成因:

1)指针变量没有被初始化

2)指针指向的内存被释放了,但是指针没有置NULL 

3)指针超过了变量了的作用范围,比如b[10],指针b+11

 

2.45 线程安全和线程不安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能多个线程先后更改数据所得到的数据就是脏数据。

 

2.46 C++中内存泄漏的几种情况

内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

1)类的构造函数和析构函数中new和delete没有配套

2)在释放对象数组时没有使用delete[],使用了delete

3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

4)没有正确的清楚嵌套的对象指针

 

2.47 栈溢出的原因以及解决方法

1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈

2)局部变量体积太大。

解决办法大致说来也有两种:

1> 增加栈内存的数目;增加栈内存方法如下,在vc6种依次选择Project->Setting->Link,在Category中选择output,在Reserve中输入16进制的栈内存大小如:0x10000000

2> 使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)

 

2.48 C++ 11有哪些新特性

C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL),并入了大部分的C++ Technical Report 1(TR1)程序库。C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、decltype,和模板的大量改进。

auto

C++11中引入auto第一种作用是为了自动类型推导

auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作

decltype

decltype实际上有点像auto的反函数,auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到类型,有实例如下:

nullptr

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,

lambda表达式类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:

[函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}

 

2.49 C++中vector和list的区别

vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

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

vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector<int>::iterator支持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不支持随机访问,因此list<int>::iterator则不支持“+”、“+=”、“<”等

vector<int>::iterator和list<int>::iterator都重载了“++”运算符。

总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;
如果需要大量的插入和删除,而不关心随机存取,则应使用list。

 

2.50 C语言的函数调用过程

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

传值:传值,实际是把实参的值赋值给行参,相当于copy。那么对行参的修改,不会影响实参的值 。
传址: 实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。

 

2.51 C++中的基本数据类型及派生类型

1)整型 int

2)浮点型  单精度float,双精度double

3)字符型 char

4)逻辑型 bool

5)控制型 void

基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。

 类型修饰符包括:

>short     短类型,缩短字长

>long      长类型,加长字长

>signed    有符号类型,取值范围包括正负值

>unsigned   无符号类型,取值范围只包括正值

 

2.52 C++线程中的几种锁机制

线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能越强大,性能就会越低。

 

2.53 string的c_str()和strcpy的差别

c_str是c++中为了兼容c语言格式字符串所提供的的借口,但是其使用有很多注意点!

int main() {
	
	string s="hello";
	const char *s1 = s.c_str();
	char s2[10];
	strcpy_s(s2, s.c_str());
	s = "ss";
	printf("%s\n", s1);//输出ss
	printf("%s\n", s2);//输出hello
	system("pause");
}

从程序的输出可以看出,c_str()形式的赋值,相当于传入了一个指针,被赋值变量的值与字符串绑定;

因此为了安全起见,多使用strcpy来拷贝字符串的值。

 

3 虚函数

3.1 虚函数表详解

参考:虚函数表详解

对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
 
这里我们着重看一下这张虚函数表。C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。(也就是说当对象实例不存在时,类的虚函数就无法访问!)

C++的虚函数大致存在:

1、一般继承(无重载)  

                 

2、一般继承(有重载) 

                 

3、多重继承(无虚函数覆盖)

4、多重继承(有虚函数覆盖)

 

3.2 关于虚函数与纯虚函数

参见:C++ 虚函数和纯虚函数的区别

纯虚函数:

virtual void funtion1()=0

需要注意一些概念:

  • 虚函数,不代表函数为不被实现的函数。
  • 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现
  • 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

 

4 C++面试题:

4.1 类指针为NULL时,仍可以访问类函数

参考:对象指针为NULL,为什么还是可以调用成员函数

示例代码如下:

#include <iostream>
using namespace std;
class aa
{
public:
    void func1(){
        cout<<"func1"<<endl;
    }
    static void func2(){
        cout<<"func2"<<endl;
    }
    virtual void func3(){
        cout<<"func3"<<endl;
    }
};
int main()
{
    aa *u=NULL;
    u->func1();
    u->func2();
 //   u->func3();
    getchar();
    return 0;
}

解释:

C++类的成员函数实际上可以认为是一个普通的函数,例如上述代码中的func1(),其定义为:

void func1() { cout<<"func1"<<endl; }

只不过在编译其看来,这个函数的样子是这样的:

void A_func1(A* this) { cout<<"func1"<<endl; }

那么在类指针为NULL时,其调用方式为:

A_func1(A*(NULL)){ cout<<"func1"<<endl; }

至于func2,其声明是静态static,显然可以调用,并且func2 是为所有类共有的;

至于func3,  其声明为虚函数,通过虚函数表V-table实现,需要通过类的实例来得到需函数表的地址,但是并没有声明对应的实例,因此显然程序会崩溃。

 

4.2 c库和系统调用的关系

这里简单讲讲两者之间的关系,更详细的可以参见:系统调用和c库之间的关系

系统调用概念:

系统调用,我们可以理解是操作系统为用户提供的一系列操作的接口(API),这些接口提供了对系统硬件设备功能的操作。这么说可能会比较抽象,举个例子,我们最熟悉的 hello world 程序会在屏幕上打印出信息。程序中调用了 printf() 函数,而库函数 printf 本质上是调用了系统调用 write() 函数,实现了终端信息的打印功能。

库函数概念:

库函数可以理解为是对系统调用的一层封装。系统调用作为内核提供给用户程序的接口,它的执行效率是比较高效而精简的,但有时我们需要对获取的信息进行更复杂的处理,或更人性化的需要,我们把这些处理过程封装成一个函数再提供给程序员,更方便于程序猿编码。

库函数有可能包含有一个系统调用,有可能有好几个系统调用,当然也有可能没有系统调用,比如有些操作不需要涉及内核的功能。可以参考下图来理解库函数与系统调用的关系。

 

4.3  int(*a[10])(int)

*a[10] 代表一个存有10个指针的数组

*a[10](int) 表示数组内部指针指向int型参的函数

int *a[10](int) 代表 该函数返回值是int

举例:

#include <stdio.h>
int a1(int i){
    printf("%d\n",i);
    return i+1;
}
int main()
{
    int (*s[3])(int);
    s[0]=a1;
    int x=s[0](3);
    printf("%d",x);
    printf("Hello World!\n");
    return 0;
}

 

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

两者的主要区别是作用域和生命周期不同。全局变量有效范围是从变量定义的位置开始到本源文件结束,而局部变量只能在自己的作用域有效。全局变量生命周期和整个程序生命周期一样,而局部变量的生命周期和函数的生命周期一样。全局变量的内存分配是静态的,在main函数前初始化,如果没有初始化,会被初始化为0。局部变量的内存分配是动态的,位于线程堆栈中,如果没有初始化的,初值视当前内存的值而定。

操作系统和编译器从变量的定义和存储区域来区分局部变量和全局变量。

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值