https://blog.csdn.net/u012414189/article/details/83856874
C++语言基础
1. 指针和引用的区别
C++之父:我为啥引入引用
“引用是别名”这个概念仅仅只是在语言级别上,深入底层的话,引用必定需要存储绑定的对象的地址信息的,所以肯定会占内存。 (引用只是一个概念,怎么实现,由编译器决定,一般实现为const指针)
- 引用并非对象,它只是为一个已经存在的对象所起的别名。 指针本身就是一个对象。
- 因此不能定义引用的引用(多级引用), 但是可以使用多级指针(int** p)。
- reference必须绑定某个对象,没有所谓null reference,因此必须在定义时赋初值。 指针可以为空,无须在定义时赋初值(在块作用域内定义的指针如果没有被初始化,将拥有一个不确定的值)。
- sizeof引用 得到的是所绑定对象的大小, sizeof指针 得到的是计算机的字长(word size , 32位/64位, 4字节/8字节)
2. 堆和栈的区别
1.申请方式: 堆是程序员申请,栈是系统自动分配
2.系统响应: 栈: 只要栈的剩余空间大于所申请的空间,系统就会为程序提供内存,否则栈溢出;
堆: 堆分配算法。。。
3.空间大小: 堆是不连续的区域,空间很大,上限取决于有效的虚拟内存; 栈是一块连续的区域,大小一般是1-2M
4.生长方向: 堆向上生长, 栈向下生长(高地址向低地址)
5.分配方式: 堆是动态分配的。
6.分配效率: 栈是系统底层数据结构,效率比较高,堆是C++函数库提供的,效率低
7.碎片问题: 栈内存是连续的,而堆在多次new和delete后会产生很多碎片
3. new和delete是如何实现的,new 与 malloc的异同处
- 属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
- 参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
- 返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
- 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
自定义类型
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
重载
C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。
- 内存区域
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
malloc/free与new/delete异同点
相同点
malloc/free与new/delete都可以用于申请动态内存和释放内存,他们申请的空间都在堆上分配。
不同点
1)操作对象不同
malloc/free是C++/C语言的标准库文件,new/delete是C++的运算符;
对非内部数据对象,malloc/free无法满足动态对象要求。对象在创建时要自动执行构造函数,对象消亡之前要自动执行析构函数,而malloc/free是库函数,不是运算符,故不在编译器控制权限之内,不能够将执行构造函数和析构函数强加于malloc/free身上。而由于new/delete是C++语言,能够完成动态内存分配和初始化工作,并能够完成清理与释放内存工作,即能够自动执行构造函数和析构函数;
2)用法不同
malloc分配内存空间前需要计算分配内存大小;而new能够自动分配内存空间;
malloc是底层函数,其函数返回值类型为void *;而new运算符调用无参构造函数,故返回值为对应对象的指针;
malloc函数类型不是安全的,编译器不对其进行类型转换、类型安全的相关检查。malloc申请空间后,不会对其初始化,要单独初始化;而new类型是安全的,因为它内置了sizeof、类型转换和类型安全检查功能,且在创建对象时,就完成了初始化工作,一般初始化调用无参构造函数;
operator new对应于malloc,且operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上;但malloc不能。
free只进行释放空间;而delete则释放空间的同时调用析构函数。
此外delete使用是注意释放数组的方法为delete []数组名。
联系
new和delete功能覆盖了malloc/free,但因C++程序常会用到C函数,而C函数只能使用malloc/free管理动态内存。此外,使用是malloc和free搭配使用,new和delete搭配使用,不能混乱使用。
作者:祚儿疯
来源:CSDN
原文:https://blog.csdn.net/u012414189/article/details/83856874
版权声明:本文为博主原创文章,转载请附上博文链接!
4. C和C++的区别
C 是面向过程的一门编程语言,C++ 可以很好地进行面向对象的程序设计。C++ 虽然主要是以 C 的基础发展起来的一门新语言,但它不是 C 的替代品,它们是兄弟关系。面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。
C++ 对 C 的增强,表现在六个方面:
- 增强了类型检查机制
- 增加了面向对象的机制
- 增加了泛型编程的机制(template)
- 增加了异常处理
- 增加了重载的机制
- 增加了标准模板库(STL)
类型检查
C/C++ 是静态数据类型语言,类型检查发生在编译时,因此编译器知道程序中每一个变量对应的数据类型。C++ 的类型检查相对更严格一些。
很多时候需要一种能够实际表示多种类型的数据类型。传统上 C 使用 void* 指针指向不同对象,使用时强制转换回原始类型或兼容类型。这样做的缺陷是绕过了编译器的类型检查,如果错误转换了类型并使用,会造成程序崩溃等严重问题。
C++ 通过使用基类指针或引用来代替 void* 的使用,避免了这个问题(其实也是体现了类继承的多态性)。面向对象
C 的结构体传递的是一种数据结构,我们只是在主函数里面对这种数据类型做某种调用。主函数的架构依然是基于函数、函数族的处理过程,即面向过程。
C++ 中最大的区别就是允许在结构体中封装函数,而在其他的地方直接调用这个函数。这个封装好的可直接调用的模块有个新名词——对象;并且也把结构体换一个名字——类。这就是面向对象的思想。在构建对象的时候,把对象的一些操作全部定义好并且给出接口的方式,对于外部使用者而言,可以不需要知道函数的处理过程,只需要知道调用方式、传递参数、返回值、处理结果。泛型编程(template)
所谓泛型编程,简而言之就是不同的类型采用相同的方式来操作。在 C++ 的使用过程中,直接 template 用的不多,但是用 template 写的库是不可能不用的。因此需要对泛型有比较深入的了解,才可以更好地使用这些库。
C++ 里面的模版技术具有比类、函数更高的抽象水平,因为模版能够生成出(实例化)类和函数。可以用来:异常处理
C 语言不提供对错误处理的直接支持,但它以返回值的形式允许程序员访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0(表示没有错误),这是一种良好的编程习惯。
C++ 提供了一系列标准的异常,定义在中,我们可以在程序中使用这些标准的异常。 函数重载 & 运算符重载
C++ 可以实现函数重载,条件是:函数名必须相同,返回值类型也必须相同,但参数的个数、类型或顺序至少有其一不同。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。大多数的重载运算符可被定义为普通的非成员函数(func(a, b) 形式调用)或者被定义为类成员函数(a.func(b) 形式调用)。标准模板库(STL)
5. C++、Java的联系与区别,包括语言特性、垃圾回收、应用场景等(java的垃圾回收机制)
两者都是面向对象的语言,两者都能实现面向对象的核心思想(封装、继承、多态)。但是由于c++为了兼容c语言,java不兼容C,它是一种完全的面向对象语言。
区别:
语言特性:
1.指针:c++有指针来访问内存,而JAVA中对于用户态,编程者无法找到指针来直接访问内存指针,只有限定版的引用,更加安全。
2.多重继承:c++支持多重继承,可以继承多个父类。JAVA不支持多重继承,但是允许一个类继承多个接口。
3.数据类型和类:Java是完全面向对象的语言,所有函数和变量都必须是类的一部分。除了基本数据类型之外,其余的都作为类对象,包括数组。对象将数据和方法结合起来,把它们封装在类中,这样每个对象都可实现自己的特点和行为。而c++允许将函数和变量定义为全局的。
4.自动内存管理:java内存支持自动对无用内存回收管理,c++需要人为的使用delete去释放内存。
5.c++支持操作符重载,但是java不支持操作符重载
垃圾回收:
java内存支持自动对无用内存回收管理,c++需要人为的使用delete去释放内存。
应用场景:
C++相对于java来看是偏底层的语言,应用场景也是一些偏底层的软件,例如:图像,客户端,桌面软件等。
JAVA则是偏向应用的语言,相对来说,生态圈较好,有一些高级特性也比较好用,一般是上层应用软件,例如移动设备的软件,web网页后台逻辑开发等等。
6. Struct和class的区别
C++中class和struct的访问控制权限不同,class默认private struct默认为public
继承:class继承默认是private继承,而struct继承默认是public继承
7. define 和const的区别(编译阶段、安全性、内存占用等)
编译阶段: define是预编译阶段展开,而const是在运行阶段使用
安全性: const常量是有数据类型的,那么编译器会对const变量的类型等安全性进行检查,但是define只是在预编译阶段展开,不会进行类型的安全检查,替换时可能产生错误。
内存占用: define不会占用内存,单纯的替换而已, const会占用内存,会有对应的内存地址。
8. 在C++中const和static的用法(定义,用途)
const成员也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
const数据成员只在某个对象的生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同对象其const数据成员的值可以不同。所以不能在类的声明中初始化const数据成员,因为类的对象没被创建时,编译器不知道const数据成员的值是什么。
const数据成员的初始化只能在构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者’static const’
static表示的是静态的。类的静态成员函数、静态成员变量是和类相关的,而不是和类的具体对象相关的。即使没有具体对象,也能调用类的静态成员函数和成员变量。一般类的静态函数几乎就是一个全局函数,只不过它的作用域限于包含它的文件中。
在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化。如:double Account::Rate = 2.5; static 关键字只能用于类定义体内部的声明中,定义时不能表示为static。
用途:
const成员函数主要目的是防止成员函数修改对象的内容,即const成员函数不能修改成员变量的值,但可以访问成员变量。
static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1.不能直接存取类的非静态成员变量,调用非静态成员函数2.不能被声明为virtual。
9. const和static在类中使用的注意事项(定义、初始化和使用)
定义: const可以在类内部定义,但是定义的位置不能初始化;static只能在类的内部声明,定义只能在类的外部,并且定义的时候不能加static关键字。(C++11开始允许静态成员变量在类内定义并初始化)
初始化: const只能在类的构造函数的初始化列表中初始化;static初始化不能在类内部初始化,必须在类外部初始化。
用途:
const成员函数主要目的是防止成员函数修改对象的内容,即const成员函数不能修改成员变量的值,但可以访问成员变量。
static成员函数主要目的是作为类作用域的全局函数。不能访问类的非静态数据成员。类的静态成员函数没有this指针,这导致:1.不能直接存取类的非静态成员变量,调用非静态成员函数2.不能被声明为virtual。
10. C++中的const类成员函数(用法和意义)
void func() const;
表明是常量成员函数,这个const表明这个函数不会改变数据成员的值。void func(const a) const;
表明是参数是常量的常量成员函数,接收的参数是常量,同时不能修改数据成员的值。
意义: 为什么这么做?
这是为了保证它能被const常量对象调用,我们知道,在定义一个对象或者一个变量时,如果在类型前加一个const,如const int x;则表示定义了一个常量,它的值不能被修改。但是创建的对象却可以调用成员函数,调用的成员函数很可能改变对象的值。所以这个时候const类成员函数就出现了。
我们把那些肯定不会修改对象的各个属性值的成员函数加上const说明符,这样,在编译时,编译器将对这些const成员函数进行检查,如果确实没有修改对象值的行为,则检查通过,之后,如果一个const常对象调用这些const成员函数,编译器将允许。
11. 计算下面几个类的大小:
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4(32位机器)/8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
1.确切地说,类只是一个类型定义,它是没有大小可言的。用sizeof运算符对一个类型名操作,得到的是具有该类型 实体的大小。
2.一个对象的大小等于所有非静态成员大小的总和,大于的部分是编译器自主添加的。
C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1.如果在类中声明了虚函数(不管是一个还是多个),在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表。而虚函数本身和其他成员函数一样,不占用对象的空间。
12. 给一个代码,求输出结果
class A
{
public:
A(int x){}
}
问:A a = 1;是否正确, 如果正确, 那么它调用了哪些函数?
这类题目更常见的是在基类和子类有不同实现方法。(虚函数相关,栗子很多,不多说了)
正确。 由于A没有显示的声明,所以可以用int型进行强制转换,编译器碰到这种情况,首先如果是没有优化的编译器,对1进行强制转换int型,然后调用默认的赋值函数,1赋值给x,然后调用构造函数构造。其次,如果是有优化的编译器,可以将1强制转换成A类型,调用一次构造函数,然后再调用默认赋值构造函数赋值给a。
13. C++的STL介绍(这个系列也很重要,建议侯捷老师的这方面的书籍与视频),其中包括内存管理allocator,函数,实现机理,多线程实现等
STL是一个c++里面非常强大的库,c11引进的,里面封装例如容器,泛型算法等。
14. STL源码中的hash表的实现
hash_table是STL中hash_map 和 hash_set 的内部数据结构,hash_table的插入/删除/查找的时间复杂度都为O(1),是查找速度最快的一种数据结构,但是hash_table中的数据是无序的,一般也只有在数据不需要排序,只需要满足快速查找/插入/删除的时候使用hash_table。
15. STL中unordered_map和map的区别
map是一种映射,这种映射是有序的,底层是使用红黑树来完成的,数据通过键值才存储,键是唯一的。
unordered_map,是一种无序的,底层是通过hash表来完成的。unordered库使用“桶”来存储元素,散列值相同的被存储在一个桶里。当散列容器中有大量数据时,同一个桶里的数据也会增多,造成访问冲突,降低性能。为了提高散列容器的性能,unordered库会在插入元素是自动增加桶的数量,不需要用户指定。每个桶都是用list来完成的。
map
优点:
- 有序性: 其元素的有序性再很多应用中都会简化很多操作。
- 红黑树: 内部实现一个红黑树使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率很高。
缺点: - 空间占用率高,每一个节点都需要额外保存父节点,孩子节点以及红黑性质,使得每一个节点都会占用大量的空间。
适用于: 对顺序有要求的问题。
unordered_map
优点: 由于使用了哈希表,因此查找速度非常快。
缺点: 哈希表的建立比较耗费时间
适用于: 查找问题16. STL中vector的实现
注意两个点:
1.vector有备用空间,当备用空间不够的时候,会重新开辟原空间两倍的空间进行重写分配。
2.vector支持随机的存取,但是最好是选择从末尾插入,因为从中间插入会导致元素的移动,带来了性能的开销。
17. vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
vector压入容器的对象都是拷贝操作,而且vector的数据存放都是连续存储的,所以在操作vector操作时,应该尽量避免对尾部操作之后的地方插入删除操作,因为这样会造成元素的移动,造成大量的开销。
频繁对vector调用push_back()会导致性能下降,这是由于系统每次给vector分配固定大小的空间,这个空间可能比用户想分配的空间大一些,但是频繁的使用push_back向容器中插入元素,会导致内存分配空间不够,会再次将整个对象的存储空间重新分配,将旧的元素移动到新的空间中,开销是非常大的。
18. C++中的重载和重写的区别
https://blog.csdn.net/xu1105775448/article/details/80118159
重载:
1.在同一个作用域下,函数名相同,函数的参数不同(参数不同指参数的类型或参数的个数不相同)
2.不能根据返回值判断两个函数是否构成重载。
3.当函数构成重载后,调用该函数时,编译器会根据函数的参数选择合适的函数进行调用。
重定义(隐藏)
1.在不同的作用域下(这里不同的作用域指一个在子类,一个在父类 ),函数名相同的两个函数构成重定义。
2.当两个函数构成重定义时,父类的同名函数会被隐藏,当用子类的对象调用同名的函数时,如果不指定类作用符,就只会调用子类的同名函数。
3.如果想要调用父类的同名函数,就必须指定父类的域作用符。
重写
也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数。
19. C ++内存管理(热门问题)
c++的内存管理延续c语言的内存管理,但是也增加了其他的,例如智能指针,除了常见的堆栈的内存管理之外,c++支持智能指针,智能指针的对象进行赋值拷贝等操作的时候,每个智能指针都有一个关联的计数器,该计数器记录共享该对象的指针个数,当最后一个指针被销毁的时候,计数器为0,会自动调用析构函数来销毁函数。
20. 介绍面向对象的三大特性,并且举例说明每一个。
面向对象的三大特性:封装、继承、多态。
封装:将很多有相似特性的内容封装在一个类中,例如学生的成绩学号、课程这些可以封装在同一个类中;
继承:某些相似的特性,可以从一个类继承到另一个类,类似生活中的继承,例如有个所有的汽车都有4个轮子,那么我们在父类中定义4个轮子,通过继承获得4个轮子的功能,不用再类里面再去定义这4个轮子的功能。
多态:多态指的相同的功能,不同的状态,多态在面向对象c++里面是通过重载和覆盖来完成的,覆盖在c++里面通过虚函数来完成的。例如鸭子的例子,所有的鸭子都有颜色,我们可以将这个颜色设置成为一个虚函数,通过继承子类对虚函数进行覆盖,不同子类中有各自的颜色,也就是有各自不同的鸭子颜色,这就是多态的典型表现之一。
21. 多态的实现(和下个问题一起回答)
22. C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(热门,重要)
多态通过覆盖和重载来完成。
虚函数分为两种,纯虚函数和虚函数,纯虚函数适用于抽象基类,不需要定义,类似一种接口,是多态的典型处理方式。
一个类如果定义了虚函数,那么编译器会自动为它加上一个虚函数表,并提供一个指向虚函数表的指针,子类通过继承,可以覆盖父类的虚函数,当用户调用虚函数的时候,会调用指针,去虚函数表中找匹配的虚函数,如果当前对象有覆盖的虚函数,则去执行覆盖的虚函数,否则执行父类的虚函数。
23. 实现编译器处理虚函数表应该如何处理
虚函数表的主要目的是提供一张表,表上记录子类父类的虚函数地址,通过虚函数表可以达到动态绑定的目的。
编译器首先在编译阶段完成虚函数表的建立,然后当给父类的指针初始化指向的是哪个子类,编译器按照绑定的子类去虚函数表中找对应子类的虚函数,并绑定,这样达到了动态绑定的目的,主要这个绑定是发生在运行的阶段。
24. 析构函数一般写成虚函数的原因
因为在继承中,我们最后要销毁对象的时候,会调用析构函数,这个时候我们希望析构的是子类的对象,那么我们需要调用子类的析构函数,但是这个时候指针又是父类的指针,所以这个时候我们也要对析构函数写成虚构函数,这样析构函数的虚属性也会被继承,那么无论我们什么时候析构,都能动态绑定到我们需要析构的对象上。
25. 构造函数为什么一般不定义为虚函数
三个原因:
1.虚函数的作用是什么?是实现部分或默认的功能,而且该功能可以被子类所修改。如果父类的构造函数设置成虚函数,那么子类的构造函数会直接覆盖掉父类的构造函数。而父类的构造函数就失去了一些初始化的功能。这与子类的构造需要先完成父类的构造的流程相违背了。而这个后果会相当严重。
2.虚函数的调用是需要通过“虚函数表”来进行的,而虚函数表也需要在对象实例化之后才能够进行调用。在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则。
3.虚函数的调用是由父类指针进行完成的,而对象的构造则是由编译器完成的,由于在创建一个对象的过程中,涉及到资源的创建,类型的确定,而这些是无法在运行过程中确定的,需要在编译的过程中就确定下来。而多态是在运行过程中体现出来的,所以是不能够通过虚函数来创建构造函数的,与实例化的次序不同也有关系。
那么虚够函数为什么可以设计成虚函数呢?由于虚函数是释放对象的时候才执行的,所以一开始也就无法确定析够函数的。而去由于析构的过程中,是先析构子类对象,后析构父类对象。所以,需要通过虚函数来指引子类对象。所以,如果不设置成虚函数的话,析构函数是无法执行子类的析构函数的。
26. 构造函数或者析构函数中调用虚函数会怎样
为什么呢?这是由于构造函数或者析构函数中调用虚函数这个时候,子类或许出于一个未初始化的状态,因为c++中父类先构造然后是子类,那么父类中构造调用子类,都没有构造,调用子类的虚函数,显然是错误的。
27. 纯虚函数
纯虚函数不需要定义,我们不能够为纯虚函数提供函数体,同样的,包含纯虚函数的基类是抽象基类,抽象基类是不能创建对象的,只能通过继承,继承子类中覆盖纯虚函数,执行自己的功能,子类是可以创建对象的。
28. 静态绑定和动态绑定的介绍
静态绑定:通过用户定义指针指向的类型来进行绑定,在编译的时候已经完成。
动态邦定:c++中虚函数的功能,通过虚函数表,在运行阶段进行绑定,即运行的时候才知道绑定的函数。
29. 引用是否能实现动态绑定,为什么引用可以实现
可以实现,因为动态绑定是发生在程序运行阶段的,c++中动态绑定是通过对基类的引用或者指针调用虚函数时发生。
因为引用或者指针的对象是可以在编译的时候不确定的,如果是直接传对象的话,在程序编译的阶段就会完成,对于引用,其实就是地址,在编译的时候可以不绑定对象,在实际运行的时候,在通过虚函数绑定对象即可。
30. 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
深拷贝就是拷贝内容,浅拷贝就是拷贝指针。
浅拷贝拷贝指针,也就是说同一个对象,拷贝了两个指针,指向了同一个对象,那么当销毁的时候,可能两个指针销毁,就会导致内存泄漏的问题。
深拷贝不存在这个问题,因为是首先申请和拷贝数据一样大的内存空间,把数据复制过去。这样拷贝多少次,就有多少个不同的内存空间,干扰不到对方。
31. 对象复用的了解,零拷贝的了解
对象复用指得是设计模式,对象可以采用不同的设计模式达到复用的目的,最常见的就是继承和组合模式了。
32. 介绍C++所有的构造函数
默认构造函数、一般构造函数、拷贝构造函数
默认构造函数(无参数):如果创建一个类你没有写任何构造函数,则系统会自动生成默认的构造函数,或者写了一个不带任何形参的构造函数。
一般构造函数:一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)。
拷贝构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回。
33. 什么情况下会调用拷贝构造函数(三种情况)
(1)用类的一个对象去初始化另一个对象时
(2)当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用
(3)当函数的返回值是类的对象或引用时
34. 结构体内存对齐方式和为什么要进行内存对齐?
1.前面的地址必须是后面的地址正数倍,不是就补齐
2.整个Struct的地址必须是最大字节的整数倍
为什么要?
空间换时间,加快cpu访问内存的效率,这是因为许多计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2、4或8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间接口的硬件设计
35. 内存泄露的定义,如何检测与避免?
内存泄漏指的是开辟的内存没有释放,或者是存在用户操作的错误,导致野指针,无法释放原来分配的内存。
工具监测:在vs里面支持CRT这个库函数,函数里面有内存监测工具,可以调用,在程序中判断内存时否有泄漏。
人为监测:观测所有new开辟内存空间的地方有没有free掉。
避免:在编程习惯上要注意使用尽量使用STL函数,使用vector而不是数组,使用智能指针而不是指针。
36. 手写实现智能指针类(34-37我没遇见过)
37. 调试程序的方法
这个方式很多,裸机程序,主动调试,gdb调试,IDE断点调试等。
38. 遇到coredump要怎么调试
内存泄漏的方法很多,可以用gdb打开core文件,确定出错的堆栈地点,从而判断程序出错的位置。
39. 内存检查工具的了解
在vs里面支持CRT这个库函数
40. 模板的用法与适用场景
模板是C11里面添加的,使用与在不知道类型的情况下,编写一个泛型的程序,模板通过用一个指定的关键字来代替类型,进行泛型编程。
应用场景:应用场景很多,例如我们要编程一些和类型无关的代码时,STL里面的很多容器都是用到了模板,容器的功能都可以使用,但并没有确定容器里面一定要用指定的类型,可以是任何的类型。
41. 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
成员初始化的概念,就是说类的成员使用在定义的时候就使用构造函数初始值列表初始化
使用成员初始化要快些,这里说的快些是比较的是赋值,如果指定义变量,没有列表初始化,那么这样变量旧会执行默认的初始化,然后在赋值,这样就多了一次赋值操作,带来的开销取决于数据成员的类型。
除了效率之外,有一些成员必须列表初始化,例如**const或者**引用**
42. 用过C11吗,知道C11新特性吗?(有面试官建议熟悉C11)
例如:auto、decltype,nullptr,for(auto i:m),lambda表达式,智能指针等。
43. C++的调用惯例(简单一点C++函数调用的压栈过程)
对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈
代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
程序开始,从main开始,首先将参数压入栈,然后压入函数返回地址,进行函数调用,通过跳转指定进入函数,将函数内部的变量去堆栈上开辟空间,执行函数功能,执行完成,取回函数返回地址,进行下一个函数。
44. C++的四种强制转换
四种强制转换是static_cast、dynamic_cast、const_cast、reinterpret_cast。
static_cast:静态强制转换,类似传统c语言里面括号的强制转换
dynamic_cast:动态强制转换,主要应用于多态,父子类的类型转换,dynamic_cast和static_cast不同的是,它会检查类型转换是否正确,不能转换,则会返回null,所以不算是强制转换。
const_cast:取出const属性,比较简单,可以把const类型转换为非conse指针类型。
reinterpret_cast:一种非常随意的二进制转换,简单理解对一个二进制序列的重新解释。
45.阅读C++语言代码输出()
1 | int main() |
输出 3 3
第4行: p=arr[0]
第5行:arr[0]=90;p自加1,此时p=arr[1];
第6行:先计算(++p),p=arr[2],在计算*p=arr[2].原因prinf从右到左编译,从左到右输出
试了一下,cout也是如此
从右向左压值,从左向右调用。
cout << a << b;
相当于cout.operator <<(a).operator <<(b);
46. 阅读c++代码输出()
1 | class base1{ |
输出: 随机数 12
初始化列表的执行顺序是变量的声明顺序
47. 64位电脑 运行c++结果输出()
1 | class A |
48. 下列选项中,不可能是快速排序第2趟排序结果的是 ()
4 14 10 12 8 6 18
4 6 10 8 12 14 18
6 4 10 8 14 12 18
6 4 10 8 14 12 18
快速排序的核心思想是基准数就位,每一趟至少有一个基准数就位,所以两趟下来至少就位两个基准数就位,C选项只有18就位,所以错误
49. 单例模式、工厂模式 描述一下实际应用场景
50. 聊天室使用UDP? 为什么?如果产生丢包怎么办?
51. X定义如下,若存在X a; a.x=0x11223344;则a.y[1]的值可能为()
union X{
int x;
char y[4];
};
联合体采用的是共享同一内存地址,且对齐,,int型为4个字节,char为1个字节,且大小为4,则该联合体一共占用4个字节的空间。
而对于int型赋值之后,即4个字节的空间上已经有了内容。
大端:低字节在高地址,0x44,0x33,0x22,0x11
小端:高字节在高地址,0x11,0x22,0x33,0x44
答案: 22, 33
52. 引用可以用 const修饰,只是没有什么作用,在VS2013上编译只是报警告而已。至于大家认为的 const int& a = b; 这种写法叫做指向const int 变量的引用,const修饰的是目标值,并不是修饰的引用。
指针可以有顶层const, const int* const p = p1; 但是 引用就没有顶层const了。
53. 友元本质上是普通函数,不在类范畴中,没有 this、成员的概念。友元类不具有传递性、继承性、双向性。
54. 任何指针都可以转化为void * , void *可以转化为任何指针
指向任意非常量对象的指针能转换成void* ;
指向任意对象的指针能转换成const void*
55. 6个苹果,每天至少吃一个,吃完为止,一共有多少种?
答案是:32.
六个一样的苹果,排成一排,之间有5个空。想像按吃的天数用隔板把它们分成每天不同的数量。
6天,相当用5个插板插入5个空中,苹果分成了6份:C(5,5)=1
5天,相当用4个插板插5个空,苹果分成了5份:C(5,4)=5
(这也不难理解:有一天是2个苹果,可以第1、2、3、4、5中的某一天吃)
4天,C(5,3)=10
3天,C(5,2)=10
2天,C(5,1)=5
1天,C(5,0)=1
总的方法为:1+5+10+10+5+1=32种.
56. int (*s[10])(int) 表示的是什么?
答案: 函数指针数组,每个指针指向一个int func(int param)的函数。
链接:https://www.nowcoder.com/questionTerminal/32372cbd0f22496481a91d9bfcdbc511
来源:牛客网
思路1
1、首先s[10] 是一个指针数组,s 是一个含有10个指针的数组,故可以这样来看这条声明语句:假设 p 等价于 s[10],声明语句变为 int (p)(int);
2、观察 int (p)(int), 从名字开始,p前面有一个 * ,因此 p 是指针,有侧是形参列表,表示p指向的是函数,在观察左侧,函数返回的是 int;
3、则 int (p)(int) 解读为:函数指针,指向一个 int func(int param) 的函数;
4、故 int (*s[10])(int) :解读为:函数指针数组,每个指针指向一个 int func(int param)的函数。
思路2
一个简单的方法,把读作 (后面的内容)is a pointer points to (*前面的内容) 这里就可以读作 s[10]is a pointer points to int (int),即指向函数的指针数组。
57. memset memcpy strcpy
58. 字符串连接函数strcat
59. 二叉排序树的查找路径

解析: 前面的节点, 或者比后面的节点都大, 或者比后面的节点都小
二叉排序树, 又叫二叉查找树, 它或者是一棵空树
判断有向图是否有环(回路)
判断是否有环方法:1.拓扑排序
2.深度优先遍历
3.广度优先遍历