C++随记

来源:互联网 发布:c语言编程入门编程题 编辑:程序博客网 时间:2024/06/06 02:31

1.c/c++的内存分配方式 
代码区:在一个exe中,正文段(Text Segment)储存指令 
全局数据区:数据段(Data Segment)储存已初始化的全局变量和静态变量,BSS段(BSS Segment)储存未赋值的全局变量所需的空间。把比较大的数组定义在main函数外。 
在程序运行时: 
堆区:动态内存。程序运行时用malloc或new申请任意多少的内存,需要用户free或delete,若没有收回内存则会产生内存泄漏。程序结束时OS会回收用户没有回收的内存空间。堆都是动态分配的,没有静态分配的堆。 
栈区:编译器自动分配,存放函数的局部变量,参数,返回数据,返回地址等。 
因为局部变量也是放在栈中的,栈溢出不一定是递归调用太多,也可能是局部变量太大。栈有两种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的。它的动态分配是由编译器进行释放,无需手工实现。栈里如果使用了stl容器 那么就说明是动态分配的内存。 
参考:/C++:程序的内存分配方式式

2.对于使用文件输入输出代替debug输入输出,查看Prime。

3.floor是对小数取整,floor(x+0.5)就是四舍五入了。 
判断一个double是不是整数:floor(x+0.5)== x,原因是因为浮点数计算可能存在误差,会把整数1变成0.9999….

4.float: 4 Bybe 对于单精度浮点数,符号1位,指数位8位,尾数23位。指数能够表示的指数范围为-128~127,尾数为23位,数值范围-3.4*10^38~+3.4*10^38。 
double:8 Bybe 双精度浮点数,符号位1位,指数位11位,表示的范围为-1024~1023,尾数52位。数值范围-1.7*10^-308~1.7*10^308。 
int、long:4 Byte -2^31 ~ 2^31 (>10^9) 
long long:8 Byte -2^63 ~ 2^63 (>10^18)

5.C++中sort()的时间复杂度。 
C中的qsort()采用的是快排算法,C++的sort()则是改进的快排算法,优于qsort的一些特点:对大数组采取9项取样,更完全的三路划分算法,更细致的对不同数组大小采用不同方法排序。两者的时间复杂度都是nlogn,但是实际应用中,sort()一般要快些,建议使用sort()。

6.STL中vector如何扩展空间(STL的底层实现) 
在调用push_back时,每次执行push_back操作,相当于底层的数组实现要重新分配大小;这种实现体现到vector实现就是每当push_back一个元素,都要重新分配一个大一个元素的存储,然后将原来的元素拷贝到新的存储,之后在拷贝push_back的元素,最后要析构原有的vector。 
参考:STL底层数据结构实现

7.深浅拷贝的定义和使用 
在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标–浅拷贝。那么要实现对象拷贝,需要自定义拷贝构造函数,即深拷贝。(下接13)

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

9.i++和++i的效率问题 
自定义数据类型的情况,++i效率较高,因为前缀式(++i)可以返回对象的引用,而后缀式(i++)必须产生一个临时对象保存更改前对象的值并返回(实现过自定义类型++运算符定义的就知道),所以导致在大对象的时候产生了较大的复制开销,引起效率降低,因此处理使用者自定义类型(注意不是指内建类型)的时候,应该尽可能的使用前缀式。

10.关于指针 
1.指针作为函数传入参数时,传入的指针仅仅是一个拷贝,方法不会改变原指针的地址、值,但是可能会改变原指针所指向内存块的数据。即(*p)可以被修改,但是(p)不会被修改。 
2.const在指针前后的区别: 
int *const p = &a; 
以上的语句,p指向的地址不能改变,但p指向的地址里面的值是可以改变的。比如*p = 2,这样a肚子里的值1就改变成2了。当然*p的值也变为2。但不能p = &b;试图想把p指向变量b。 
int const *p = &b; 
这条语句与上面的相反。p指向的地址可以改变,但它的值不能直接改变。也就是说可以执行p = &a这样的操作,因为这只是改变p指向的地址。不能执行*p = 2这样的操作,因为这样是直接改变值。 
根据这个特性来判断传入的指针参数需不需要加const,加在指针前面还是后面。 
3.当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针(即对参数求sizeof得到的结果是4) 
4.类的对象在定义时会调用类的默认构造函数,并且分配栈的内存,求sizeof是类的大小,当作用域结束后会自动调用析构函数。 
类的指针如果在定义时需要用new初始化才会调用构造函数,分配堆上的内存,求sizeof是指针的大小(4字节或8字节),当作用域结束后需要delete才会调用析构函数,所以指针用完后一定要delete归还。 
5.如果定义了一个指针但是没有使用new分配空间,仅仅是把指针指向某一个对象,那么不需要使用delete。因为指针的定义是在栈上分配空间的,系统会自动回收该指针,如果指针仅仅是指向,回收的也是指针,而指针指向的那个对象由管理它的代码进行回收。(如定义一个数组int a[10],然后定义一个指针int* A=a,则A不需要delete,如果delete会把a[10]数组给delete掉,其他地方用的时候就会报错) 
6.如果定义了两个指针并且它们的初始化指向的内容相同,那么这两个指针相等(如char* str1 = “abc”;char* str2 = “abc”;)。那么内存中只有一份”abc”,并且两个指针同时指向它,此时str1=str2。而且,此时 
“abc”是存放在静态数据区的,所以不能通过*str1来修改静态数据区的值。只有定义了一个char数组,然后指针指向这个数组时,可以通过*str1来修改,因为数组是存放在栈空间上的)。 
6.指针是指向一块空间的一个变量,而引用是一块空间的另一个名字。指针可以为空且可以被修改,但是引用不能为空,而且引用的值不能被修改。

11.定义一个空的类型,里面没有任何成员变量和成员函数,对该类型求sizeof,得到的结果是? 
答案是1,空类型的实例中不包含任何信息,本来应该是0,但是声明实例的时候,它必须在内存占有一定的空间,VS中每个空类型的实例占1字节。 
如果在该类型中添加一个构造函数和析构函数,再求sizeof答案是多少? 
答案还是1,调用构造函数和析构函数只需要知道函数的地址,而这些函数的地址与类型相关,与类型的实例无关,编译器也不会因为这两个函数而在实例中添加任何额外的信息。 
如果把析构函数标记成虚函数呢? 
C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位机器上,一个指针占4字节;而64位机器上,一个指针占8字节。然而32,64的判定不取决于你的计算机,而取决于你编译代码的平台。你在编译的时候,可以选择32(x86),64(x64)。

12.关于虚函数(多态的实现原理) 
虚函数表的结构:它是一个函数指针表,每一个表项都指向一个函数。任何一个包含至少一个虚函数的类都会有这样一张表。需要注意的是vtable只包含虚函数的指针,没有函数体。实现上是一个函数指针的数组。虚函数表既有继承性又有多态性。每个派生类的vtable继承了它各个基类的vtable,如果基类vtable中包含某一项,则其派生类的vtable中也将包含同样的一项,但是两项的值可能不同。如果派生类重载(override)了该项对应的虚函数,则派生类vtable的该项指向重载后的虚函数,没有重载的话,则沿用基类的值。并且这里需要注意的是基类与子类的虚函数表中的函数顺序是相同的。 
每一个类只有唯一的一个vtable,不是每个对象都有一个vtable,而每个同一个类的对象都有一个指针,这个指针指向该类的vtable(当然,前提是这个类包含虚函数)。(在类对象的内存布局中,首先是该类的vtable指针,然后才是对象数据。这样通过指针或者引用就很容易找到虚函数表)那么,每个对象只额外增加了一个指针的大小,一般说来是4字节。 
参考链接: 
http://blog.csdn.net/my765089223/article/details/8138611 
构造函数不能是虚函数,因为构造函数是给当前类分配空间的,如果没有调用构造函数就没有当前类,也就不存在空间是保存虚函数表。 
析构函数可以且应该是虚函数,这样用父类指针创建子类对象时,就不会造成内存泄露。

13.关于拷贝构造函数 
一般拷贝构造函数定义传参都是A(const A& other),就是把传值参数改为常量引用。直接A(A other)编译器会报错,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,会形成永无止境的递归调用导致栈溢出。

14.struct和class的区别 
C++中如果没有标明成员函数或者成员变量的访问级别,在struct中默认是public,在class中默认是private。 
但是在C#中,struct和class默认都是private。 
struct和class的区别是struct定义的是值类型,值类型的实例在栈上分配内存;而class定义的是引用类型,引用类型的实例在堆上分配内存。
参考链接: 
http://www.cnblogs.com/findumars/p/5006172.html

15.一般情况下如果对数组进行一些调整需要移动数组中某些数字的位置的,从数组后面开始遍历能避免某些数字的重复移动。 
如下面的空格替换,时间复杂度只需要O(n): 
这里写图片描述

16.关于链表 
1.如何判断一个链表是不是有环。 
设定两个指针,从头指针开始分别前进1步,2步。如果存在环,那么两者相遇;如果不存在环,走得快的先遇到NULL。 
原理:n%n = (2n)%n = 0,即有环的话,慢的会走一圈回到起点,而快点会走两圈回到起点,它们会在起点相遇。 
2.如何判断两个链表是否相交 
先遍历第一个链表到他的尾部,然后将尾部的next指针指向第二个链表(尾部指针的next本来指向的是null)。这样两个链表就合成了一个链表,判断原来的两个链表是否相交也就转变成了判断新的链表是否有环的问题了。只要用第一种方法在第二个链表头部开始做,这个面试的时候叫什么来着。。活学活用 
当然,有比较简单的: 
仔细研究两个链表,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。 
参考链接: 
http://blog.csdn.net/jiary5201314/article/details/50990349 
3.从尾到头打印链表 
如果可以修改数据,把链表中每个指针反过来,如果不可以,用栈。

17.可以用两个队列实现一个栈,也可以用两个栈实现一个队列。

18.关于构造函数的调用顺序。 
先调用父类的构造函数,然后在调用子类的构造函数,另,如果子类中有定义其他类的话,会先调用其它类的构造函数再调用自身的构造函数,原因是类中有成员类时,要先给成员类分配空间。而析构函数就是完全与构造函数的调用顺序相反。 
参考连接:http://blog.csdn.net/u013467442/article/details/48682663

19.关于宏定义 #define 
C文件进行编译时,实际经过了预处理、编译、汇编和连接几个过程。其中预处理器产生编译器的输出,它实现以下的功能: 
(1)文件包含 
可以把源程序中的#include 扩展为文件正文,即把包含的.h文件找到并展开到#include 所在处。 
(2)条件编译 
预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外,通常把排除在外的语句转换成空行。 
(3)宏展开 
预处理器将源程序文件中出现的对宏的引用展开成相应的宏 定义,即本文所说的#define的功能,由预处理器来完成。 
经过预处理器处理的源程序与之前的源程序有所有不同,在这个阶段所进行的工作只是纯粹的替换与展开,没有任何计算功能,所以在学习#define命令时只要能真正理解这一点,这样才不会对此命令引起误解并误用。 
为什么要使用宏定义: 
(1) 方便程序的修改

使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。我们所说的常量改变不是在程序运行期间改变,而是在编程期间的修改,举一个大家比较熟悉的例子,圆周率π是在数学上常用的一个值,有时我们会用3.14来表示,有时也会用3.1415926等,这要看计算所需要的精度,如果我们编制的一个程序中 要多次使用它,那么需要确定一个数值,在本次运行中不改变,但也许后来发现程序所表现的精度有变化,需要改变它的值, 这就需要修改程序中所有的相关数值,这会给我们带来一定的不便,但如果使用宏定义,使用一个标识符来代替,则在修改时只修改宏定义即可,还可以减少输入 3.1415926这样长的数值多次的情况,我们可以如此定义 #define   pi   3.1415926,既减少了输入又便于修改,何乐而不为呢?

(2) 提高程序的运行效率

使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生函数调用时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。

参考链接:http://blog.chinaunix.net/uid-21372424-id-119797.html

0
 
0
原创粉丝点击