41.C++中有几种类型的new
在C++中,new有三种典型的使用方法:plain new,nothrow new 和placement new
1.plain new
就是普通的new,定义如下:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的。
2.nothrow new
在空间分配失败的情况下是不抛出异常的,而是返回NULL。
void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
3.placement new
允许在一块已经分配成功的内存上重新构造对象或者对象数组。不用担心内存分配失败,因为它根本不分配内存,他唯一做的一件事就是调用对象的构造函数。
void* operator new(size_t,void*);
void operator delete(void*,void*);
使用它要注意的两点:
主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组。
构造起来的对象数组,要显式的调用它们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete构造起来的对象或数组大小并不一定等于原来的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行错误。
42.C++的异常处理方法
在程序执行过程中,由于程序员的疏忽或者是系统的资源紧张等因素都有可能导致异常,任何程序都无法保证绝对的稳定,常见的异常有:
数组下标越界
除法计算时除数为0
动态分配空间时空间不足
......
1. try、throw和catch关键字
C++中的异常处理机制主要使用这三个关键字,其在程序中的用法如下:
#include <iostream>
using namespace std;
int main()
{
double m = 1, n = 0;
try {
cout << "before dividing." << endl;
if (n == 0)
throw - 1; //抛出int型异常
else if (m == 0)
throw - 1.0; //拋出 double 型异常
else
cout << m / n << endl;
cout << "after dividing." << endl;
}
catch (double d) {
cout << "catch (double)" << d << endl;
}
catch (...) {
cout << "catch (...)" << endl;
}
cout << "finished" << endl;
return 0;
}
//运行结果
//before dividing.
//catch (...)
//finished
代码中,对两个数进行除法运算,其中除数为0。可以看到以上三个关键字,程序的执行流程是先执行try包裹的语句块,如果执行过程中没有异常发生,则不会进入任何catch包裹的语句块,如果发生异常,则使用throw进行异常抛出,再由catch进行捕获,throw可以抛出各种类型的信息,代码中使用的是数字,也可以自定义异常class。catch根据throw抛出的数据类型进行精确捕获(不会出现类型转换),如果匹配不到就直接报错,可以使用catch(...)的方式捕获任何异常(不推荐)。当然,如果catch了异常,当前函数如果不进行处理,或者已经处理了想通知上一层的调用者,可以在catch里面在throw异常。
2.函数的异常声明列表
有时候,程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所能抛出的异常的列表,写法如下:
int fun() throw(int,double,A,B,C){...};
这种写法表名函数可能会抛出int,double或者A,B,C三种类型的异常,如果throw中为空,表明不会抛出任何如果没有throw则可能抛出任何异常。
3.C++标准异常类exception
C++标准库中有一些类代表异常,这些都是从exception类派生来的。
bad_typied:使用typeid运算符,如果其操作是一个多态类的指针,而该指针的值为NULL,则会抛出异常。
bad_cast:在用dynamic_cast进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换不是安全的,则会抛出异常。
bad_alloc:在用new运算符进行动态内存分配时,如果没有足够的内存,则会引发此类异常。
out_of_range:用vector或string或at成员函数访问根据下标访问元素时,如果下标越界,则会抛出异常。
43. static的用法和作用?
1.隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
2.保持变量内容的持久:存储在静态数据驱的变量会在程序刚开始运行时就完成初始化,也是唯一一次初始化。
3.默认初始化为0:静态数据区,内存中所有的字节默认值都是0x00.
4.C++中的类成员声明static。
44.指针和const的用法
当const修饰指针时,由于const位置不同,它修饰的对象会有所不同。
int *const p2中const修饰p2的值,所以理解为p2的值不可以改变。但是可以通过*p2读写这个变量的值。顶层指针表示指针本身就是一个常量。
int const *p1或者const int *p1两种情况中const修饰*p1.所以理解为*p1的值不可以改变。
45.形参与实参的区别?
形参变量只有在被调用时才分配内存单元,在调用结束后,即刻释放所分配的内存单元。
实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。
实参和形参在数量上,类型上,顺序上应该严格一致,否则会发生“类型不匹配”的错误。
函数调用中发生的数据传送是单向的。因此,在函数调用过程中,形参的值发生改变,而实参的值不会变化。
当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中处于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候释放形参,而实参的内容不会发生变化。
46.值传递、指针传递、引用传递的区别和效率
值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。(传值)
指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)
引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名(传地址)
效率上讲,指针传递和引用传递比值传递效率更高,一般主张使用引用传递,代码逻辑上更加紧凑、清晰。
47.静态变量什么时候初始化
初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。
C和C++中静态变量的初始化节点有点不太一样:C中初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,因此C中无法使用变量对静态局部变量进行初始化;C++中初始化时发生在相关代码执行的时候,主要是由于C++引入对象后,要进行初始化必须执行相应构造和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单的分配内存。
48.const关键字的作用有哪些?
阻止一个变量被改变。
对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时指定为const。
对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数。
对于类的成员函数,有时候必须指定其返回值类型为const类型,以使得其返回值不为左值。
const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以const对象内的所有数据成员。
49.什么是类的继承?
继承:一个类继承了另外一个类的属性和方法,这个心累包含了上一个类的属性和方法。
继承的特点:子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用。
50.从汇编层去解释一下引用
9: int x = 1;
00401048 mov dword ptr [ebp-4],1
10: int &b = x;
0040104F lea eax,[ebp-4]
00401052 mov dword ptr [ebp-8],eax
x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。
lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器。
mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址。
51.深拷贝和浅拷贝可以描述一下吗?
浅拷贝:仅仅是指向被拷贝的内存地址,如果原地址中的对象被改变了,那么浅拷贝出来的对象来会被相应改变。
深拷贝:在计算机中开辟了一块新的内存地址用于存放赋值的对象。
52.new和malloc区别?
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
使用new操作符申请内存分配时无需指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式指出所需要的内存大小。
new是符合类型安全性的操作符,返回值是对象类型的指针。类型严格与对象匹配,无需类型转换;malloc返回void *,需要进行强制类型转换。
new失败会抛出bad_alloc异常,malloc失败返回NULL。
new会先调用operator new函数,申请足够的内存,然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
53.delete p,delete []p,allocator 都有什么作用?
动态数组管理new一个数组时,[]中必须是一个整数,但不一定是一个整数常量,普通数组必须是一个常量整数。
new动态数组返回的并不是数组类型,而是一个元素类型的指针。
delete[]时,数组中的元素按逆序的顺序进行销毁。
new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开,先申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
54.new和delete的实现原理,delete是如何知道释放内存的大小的?
需要再new[]一个对象数组时,需要保存数组的维度,C++的做法是在分配数组空间时多分配了4个字节的大小,专门保存数组的大小,在delete是就可以取出这个保存的数,就知道需要调用析构函数多少次了。
55. malloc申请的存储空间能用delete释放吗?
不能,,malloc/free主要是为了兼容C,new和delete完全可以取代。
malloc/free的操作对象都是必须明确大小的,而且不能用在动态类上。
new和delete会自动进行类型检查和大小,malloc和free不能执行构造函数和析构函数,所以动态对象是不行的。
56.malloc与free的实现原理?
标准C库中,malloc、free底层是由brk,mmap,munmap这些系统调用实现的。
malloc小于128K的内存,使用brk分配内存,将_edata往高低址推,malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高低址释放之后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128k,执行内存紧缩操作。
malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。
57. malloc,realloc, calloc的区别
1. malloc
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;
2.calloc
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
省去了人为空间计算,malloc申请的空间值是随机初始化的,calloc申请的空间值初始化为0.
3.realloc
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。
58.类成员函数初始化方法?构造函数的执行顺序?为什么用成员初始化列表会快一些?
1.赋值初始化(通过在函数体内进行赋值初始化)和列表初始化(在冒号后使用初始化列表进行初始化)。
区别在于:赋值初始化实在所有的数据成员被分配内存空间后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化的。
2.一个派生类构造函数的执行顺序如下:
虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)
基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)
类类型的成员对象的构造函数(按照成员对象在类中的定义顺序)
派生类自己的构造函数
3.赋值初始化是在构造函数中做赋值的操作,而列表初始化是做纯粹的初始化操作。因为C++的赋值操作是会产生临时对象的,临时对象的出现会降低程序的效率。
59.有哪些情况必须用到成员列表初始化?作用是什么?
四种情况必须使用成员初始化:
当初始化一个引用成员时
当初始化一个常量成员时
当调用一个基类的构造函数,而他拥有一组参数时
当调用一个成员类的构造函数,而他拥有一组参数时
作用:
编译器会操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何式示用户代码之前。
list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的。
60.C++中新增了string,它与C语言中的char*有什么区别?是如何实现的?
string继承自basic_string,其实是对char *进行了封装,封装的string包含了char*数组,容量,长度等属性。
string可以进行动态拓展,在每次拓展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容。