C++基础知识总结

  1. 堆与栈的区别

c++中内存分为栈,堆,全局/静态存储区,常量存储区, 代码区。

栈:(1)由编译器负责分配释放,用于存储局部变量,函数参数等。 (2)内存连续,先入后出,不会产生内存碎片。

堆: (1)由程序员负责分配释放,如new, malloc生成堆内存。如果忘记回收会造成内存泄漏,直到程序结束才会被释放掉。
(2)内存不连续,频繁的new/delete会产生大量碎片,降低程序效率。

  1. 指针函数和函数指针

指针函数:返回指针的函数,一般指针从参数传过来或函数内new出来,不能返回局部变量指针。

函数指针:指向函数的指针。


//返回值 (*变量名)(参数...);  返回值 ()(参数...)就是变量的类型。 
int sum(int a, int b){return a+b;}  
int (*p)(int,int) = sum; 那么时对p的赋值。

typedef int (*p)(int,int); 因为返回值 ()(参数...)指类型,所以p现在成了这个类型的别名。 p func = sum;func成为函数指针。
  1. 继承

重载: 同一类中函数名相同,参数列表不同;
覆盖: 派生类重新定义和基类函数名相同的函数(即使参数列表不同),就完成了对基类函数的覆盖,虚函数和非虚函数都是如此。所以尽量使用override,通过ide的语法检查保证覆盖的函数参数列表一致性。成员变量也可以覆盖。

虚函数覆盖原理:把基类虚函数表中相应函数的地址换成了子类函数地址。
非虚函数覆盖原理:在编译期间要把非虚函数的调用语句全部换成函数地址,所以覆盖后这个函数地址就换成了子类中的函数。
由此看来覆盖之后函数定义依然存在,存在于代码区内存,只是间接访问的地址变了,所以我们仍然能通过指定基类作用域的方式,调用基类函数。

class A {
public:
	 void func()
	{
		cout << "a";
	}
};
class B :public A {
public:
	void func(int i) //在定义覆盖函数时,注意override才能避免参数列表写错。
	{
		cout << i << endl;
	}
};

int main(int argc, char** argv)
{
	B b;
	b.func(0);
	b.A::func();

	system("pause");
}

派生类继承的内容: 非private的非虚函数,虚函数,静态变量和函数。注意派生类和基类的同名静态变量共用一块内存。如果派生类不对基类函数隐藏,
那么就可以使用派生类对象直接调用基类的函数。

  1. 多态性

一种接口有多种实现。编译时多态,由重载体现,相同函数名不同参数列表;运行时多态,由虚函数体现。

非虚函数的地址是编译期间就确定,虚函数地址运行期间绑定。会根据基类指针指向的对象得到虚表指针,然后根据虚表指针访问类的虚函数表,从而获得虚函数的地址,然后调用执行。

虚表的构建与需函数的调用过程参考:https://blog.csdn.net/Xiongchao99/article/details/73381280

所以调用非虚函数看变量类型,调用虚函数看对象。例如,

#include <iostream>
using namespace std;
 
class A
{
public:
	virtual void x()
	{
		cout << "A::x" << endl;
	}
	
	void y()
	{
		x();
		cout << "A::y" << endl;
	}
};
 
class B : public A
{
public:
	virtual void x()
	{
		cout << "B::x" << endl;
	}
	
	virtual void y()
	{
		cout << "B::y" << endl;
	}
};
 
 
int main() 
{
	A *p = new B;
	p->y();
	return 0;
}
结果:
B::x
A::y

虽然y()在B中是虚函数但在A中不是,也就是说只有调用B子类的该虚函数才会体现他的多态性。
这里调用y非虚函数,根据变量类型会调用A中的y(),然后因为x()是虚函数,根据对象类型A调用A中x();

如果在父类中是虚函数,那么子类不用加virtual也是虚函数。如果只在子类才是虚函数,那么从子类的子类开始才会体现多态。

普通函数为什么比虚函数调用快?

因为普通函数是静态联编的,而虚函数地址是动态联编的。

静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。

为什么子类和父类的函数名不一样,还可以构成重写呢?
因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。

  1. 浅拷贝和深拷贝

发生拷贝的情况:
(1)对象以值传递的方式传入函数体 (2)对象以值传递的方式从函数返回 (3)对象通过另外一个对象初始化或赋值。

浅拷贝只是拷贝了类成员的值。当对象中含所有指针变量时,浅拷贝只拷贝了指向同一块内存区域的指针,当其中一个对象对这块内存内存进行改变另一个对象也会受到影响,当一个对象释放掉这块内存,另一个对象的指针变量就会变成野指针,容易发生错误。而深拷贝会定义拷贝构造函数和赋值运算符重载,重新拷贝一份内存,是两个对象之间不会产生影响。

  1. 纯虚函数
    定义时虚函数后加=0。有纯虚函数的类是抽象类,不能生成对象。纯虚函数用于不方便生成对象的类,把纯虚函数交给子类实现。

  2. const
    const修饰变量:表示只读变量,根据初始化判断编译期常量还是运行期常量。
    const修饰指针变量(const T*p情况),最好是const指针给const指针赋值,非const指针给非const赋值。

//非const赋给const,const失去意义。
int i=10;
const int* p = &i;

cout << *ii << endl;//10
i = 20;
cout << *ii << endl;//20
//const赋给非const,语法错误

const修饰函数返回值:避免给表达式赋值(参考返回值引用)
const修饰成员函数:不允许在函数内部修改对象,const不能修饰static成员函数,因为const是为了保证对象不被修改,而static函数无对象。

  1. static与全局变量

全局变量:定义在函数体外的变量(函数同理)就是全局变量,具有全局的作用域和生命周期。其他源文件想使用某个源文件中定义的全局变量,只需要extern 类型名 变量;声明即可。

静态变量:存储于全局/静态存储区,生命周期与程序相同,但作用域由定义位置决定,所以可以用static修饰全局变量,将全局变量作用域限定在当前源文件中。

static修饰类成员时,static属于类,类和派生类的所有对象都可以访问static成员变量,当然是在作用域允许的情况下(protected,public)。static成员变量必须在类外初始化,static成员函数可通过类名直接调用。

class A
{
public:
	static void func() { cout << "stattuic"; }
	const static int var;
	static int var1;
};

const int A::var = 1;
int A::var1 = 1;

class B :public A {

};
int main()
{
	A a;
	B b;
	cout << b.var << endl;
	cout << b.var1 << endl;
	A::func();

	return 0;
}

由此可见静态变量和全局变量生命周期相同,但是static的作用域和普通变量是一样的。

  1. 32位,64位系统中,各种常用内置数据类型占用的字节数?

char1个字节(固定)
(*即指针变量): 4个字节(32位机的寻址空间是4个字节。同理64位编译器8字节)(随系统变化)

short int : 2个字节(固定)

int4个字节(固定)

unsigned int : 4个字节(固定)

float: 4个字节(固定)

double: 8个字节(固定)

long: 4个字节,648个字节(随系统变化)

unsigned long: 4个字节,648个字节(随系统变化)
long long: 8个字节(固定)


64位操作系统

char1个字节(固定)

*(即指针变量): 8个字节

short int : 2个字节(固定)

int4个字节(固定)

unsigned int : 4个字节(固定)

float: 4个字节(固定)

double: 8个字节(固定)

long: 8个字节

unsigned long: 8个字节(变化*其实就是寻址控件的地址长度数值)

long long: 8个字节(固定)*long 不同其余均相同。
  1. C++类中数据成员初始化顺序?
    1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。

3.类中const成员常量必须在构造函数初始化列表中初始化。

4.类中static成员变量,只能在类内外初始化(同一类的所有实例共享静态成员变量)。

初始化顺序:

1) 基类的静态变量或全局变量
2) 派生类的静态变量或全局变量
3) 基类的成员变量
4) 派生类的成员变量

const成员变量,引用成员变量, 其他类的对象必须在初始化列表中初始化。

  1. static_cast, dynamic_cast, const_cast, reinpreter_cast的区别

static_cast相当于c语言的强制类型转换.

dynamic_cast要求转换类型必须是指针或引用,而且基类一定要包含虚函数。在上行转换(子类对象转换为基类类型)中和static_cast效果是一样的,下行转换时要把父类类型转换为子类类型,这时父类指针一定要指向子类对象转换才是安全的。如果使用static_cast不会进行安全性检查,dynamic_cast会安全性检查,如果父类指针没有指向子类对象则返回null指针。

class A
{
public:
	virtual void f() { cout << "A::f"; }
	
};
class B :public A {
public:
	void f() {
		cout << "B::";
	}
	int a;
};
int main()
{
	//上行转换,安全
	B* b = new B;
	A* a = static_cast<A*>(b);
	a->f();//satic_cast和dynamic——cast效果一样,都输出B::f
	
	//下行转换,安全
	A* a = new B;
	B* b = static_cast<B*>(a);//或者dynamic_cast
	cout << b->a;
	a->f();
	//下行转换,不安全
	A* a = new A;
	B* b = static_cast<B*>(a);
	cout << b->a; //可能访问到不确定,也可能无效内存,反正不是b成员变量的值,只是相对b对象内存偏移一定位置内存的值。
	a->f();

	B* b = dynamic_cast<B*>(a);//返回nullptr
	return 0;
}

reinterpret_cast可以对无关类指针进行转换,甚至可以直接将整型值转成指针,这种转换是底层的,有较强的平台依赖性,可移植性差;

const_cast可以将常量转成非常量,但不会破坏原常量的const属性,只是返回一个去掉const的变量。

  1. 定义一个空类编译器做了哪些操作

一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,一般编译过就相当于:

class Empty
{
public:
Empty(); // 缺省构造函数//
Empty( const Empty& ); // 拷贝构造函数//
~Empty(); // 析构函数//
Empty& operator=( const Empty& ); // 赋值运算符//
};
  1. 智能指针

用对象管理指针,防止再分配堆内存时忘记释放内存而引起内存泄漏。使用对象管理指针的话,对象生命周期结束时,指针所指向的内存被自动释放,不用程序员担心。

shared_ptr: 将原始指针分配给多个所有者,并用引用计数来记录所有者的个数。每当一个所有者结束生命周期时,引用计数减一,当引用计数为0时,释放原始指针所指向的内存。

用拷贝构造函数和make_shared初始化;支持拷贝语义(拷贝构造和赋值)。
reset(pointer)原先指针引用计数减一,新的指针引用计数加一。

	shared_ptr<int> p1 = make_shared<int>(10);
	shared_ptr<int> p2(p1);
	cout << p1.use_count() << endl; //2

 p2 = p1;

unique_ptr: 原始指针只能有一个所有者,当所有者对象生命周期结束,指针所指向的内存也被释放。

用new的方式直接初始化;不支持拷贝语义,只支持移动语义。
release()返回原始指针,解除对原始指针的所有权;
reset(pointer)释放原先指针所指向的内存,指定新的指针。

int main()
{
	unique_ptr<int> p1(new int(1));
	unique_ptr<int> p2;
	p2.reset(p1.release());//常见用法,把p1的所有权转移到p2.等价于 p2 = std::move(p1);

	cout << *p2 << endl;
	cout<< p1;//智能指针不指向任何指针时,指向nullptr.
	return 0;
}

两个对象互相使用一个shared_ptr成员变量指向对方会造成循环引用。

class B;
class A
{
public:
	shared_ptr<B> data;
};

class B
{
public:
	shared_ptr<A> data;
};

int main()
{
	shared_ptr<A> a(new A);
	shared_ptr<B> b(new B);
	a->data = b;
	b->data = a;

	_CrtDumpMemoryLeaks();
	return 0;
}
  1. 内联函数和宏定义的区别
    宏定义不是函数不会执行语法检查,只是在预处理阶段进行简单的字符串替换。

内联函数本质上是函数,但内联函数体复杂时,编译器自动把内联变成普通函数。内联是在编译期间插入到代码中。省去压栈退栈过程提高了效率。

编译过程:预处理 - 编译-汇编- 链接
预处理:处理头文件,宏定义。
编译:将上一部产生的文件和源文件转化为汇编
汇编:将汇编语句转化为机器码,也就是obj文件。
链接: 链接obj文件成可执行文件。

参考:
https://www.cnblogs.com/inception6-lxc/p/8686156.html

  1. map为什么使用红黑树:

红黑树是一种二叉查找树。二叉搜索树的查找复杂度是树的高度,当查找树左右子树深度严重不平衡时退化为链表时,查找次数就变成元素个数。所以平衡二查搜索树是最快的,但是平衡树有过多的旋转操作。红黑树是在每个节点增加表示颜色的字段,红黑树确保从根节点到叶子节点任一条路径不比其他路径长两倍,因此红黑树是一种若平衡二叉树,旋转次数较少。

map使用的红黑树和哈希表有什么区别:

(1)map内部是有序状态,所以对于自定义类型的key,要实现比较函数(小于)。哈希表内部是无序的状态,对于自定义类型key要实现hash函数和等于函数。
(2)map的查询,插入删除时间都是logn,而哈希表的查找时间是常数级的O(1).哈希表的时间消耗在构建时和计算hash函数时。
(3)当数据量大的时候用哈希表速度更快,但是占内存较大,如果对内存有限制的话,使用红黑树。

hash占内存大:因为要分配足够多内存存储数组,很多槽可能是没用到的。而红黑树只为使用到的节点分配内存。

  • 哈希函数
    除留余数法
    直接寻址法
    平方取中法
    随机数法

  • 解决冲突
    开放寻址法,链表法。

参考:
https://blog.csdn.net/zzhang_12/article/details/81173891

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值