1.声明与定义的区别
int a
定义变量需要为变量在内存中分配存储空间
extern int a
声明不需要分配存储空间
声明的目的是为了在定义之前使用,如果不需要在定义之前使用,那么就没有单独声明的必要
2.static来声明一个变量的作用
局部变量用static声明,变量由动态存储方式改变为静态存储方式。为该变量分配的存储空间会存在于整个程序运行过程中。静态局部变量作用域局限于本函数内。
外部变量用static声明,使变量局部化(局部于本文件),但仍为静态存储方式。静态外部变量作用域局限于本文件内
static静态变量虽然和整个程序共生存期,但是作用域还是需要看其定义的地方,当你在某个函数中定义一个变量,该变量作用域仅在该函数中。但你在文件开头定义一个全局变量,该变量作用域仅在该文件中。所以当你声明一个变量调用另一个文件静态变量,编译器会报错的。
3.全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
全局变量储存在静态存储区,局部变量存在于堆栈中。动态申请数据存在于(堆)中。
4.局部变量能否和全局变量重名?
能,局部会屏蔽全局。要用全局变量,需要使用"::"
5.全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
可以,在不同的C文件中以static形式来声明同名全局变量。前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。
变量的定义只能出现一次,否则会导致重复定义。但却可以声明多次。全局变量定义在头文件中。当该头文件被多个c文件包含的话,就会导致重复定义。所以全局变量不可以定义在头文件中。
6.static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?
-
static全局变量:静态全局变量限制了其作用域,只在定义该变量的源文件内有效。在同一源程序的其它源文件中不能使用它。static全局变量只初始化一次,防止在其他文件单元中被引用。
-
普通全局变量:非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
-
static局部变量:变量由动态存储方式改变为静态存储方式。static局部变量只被初始化一次,下一次依据上一次结果值。
-
普通局部变量:还是动态存储方式,存储在堆栈中。
-
static函数:作用域仅在本文件中,只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。static函数在内存中只有一份。
-
普通函数:可在当前源文件以外使用,需应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。普通函数在每个被调用中维持一份拷贝。
7.extern 和 static 的区别,什么情况用前者什么情况用后者
auto自动变量:表明变量自动具有本地范围,在离开作用域,无论块作用域,文件作用域还是函数作用域,变量都会被程序隐藏或自动释放。然后等你重新进入该作用域,变量又重新被定义和调用。使用auto变量优势是无需考虑变量是否被释放。
static静态变量:简单说就是在函数等调用结束后,该变量也不会被释放,保存的值还保留。即它的生存期是永久的,直到程序运行结束,系统才会释放,但也无需手动释放。
extern外部变量:它属于变量声明,extern int a和int a的区别就是,前者告诉编译器,有一个int类型的变量a定义在其他地方,如果有调用请去其他文件中查找定义。
关于extern变量声明使用,例如一个工程中:
Test1.cpp文件开头定义了int i =10
; //定义了一个全局变量
Test2.cpp文件中定义:extern int i
; //声明在另一个编译单元有i变量
注意:不可以写成extern int i =10,因为变量已经存在,不可以在声明时候赋初始值。
8.x=x+1,x+=1,x++哪个效率高
x++ > x+=1 > x=x+1
- x++:读取x的值->x自增1
- x+=1:读取右x的地址->x值+1->将得到的值传回给x(x地址已经读出)
- x=x+1:读取右x的地址->x值+1->读取左x的地址(并不知道左右是同一个x,所以要读取两遍)->将右值传给左值
9.const 和#define 的优缺点
使用const关键字来声明变量,表明,内存被初始化后,程序便不能再对它进行修改。 在默认的情况下,全局变量的链接性为外部的,但const全局变量的链接性为内部的。也就是说,在C++看来,全局const定义就像使用了static说明符一样。
const int Months = 12; 此时,应该注意的是应该在声明中对const进行初始化,我们应该避免如下的写法: const int Months; Months = 12;
- define由预处理程序处理,const由编译程序处理。
- #define不分内存,因为它是预编译指令,编译前进行了宏替换。
- const定义常量是有数据类型的,这样const定义的常量编译器可以对其进行数据静态类型安全检查,而#define宏定义的常量却只是进行简单的字符替换,没有类型安全检查,且有时还会产生边际效应。
- 有些调试程序可对const进行调试,但不对#define进行调试
- const在编译期间会计算其值,而define不会 -当定义局部变量时,const作用域仅限于定义局部变量的函数体内。但用#define时其作用域不仅限于定义局部变量的函数体内,而是从定义点到整个程序的结束点。但也可以用#undef取消其定义从而限定其作用域范围。只用const定义常量,并不能起到其强大的作用。const还可修饰函数形式参数、返回值和类的成员函数等。从而提高函数的健壮性。因为const修饰的东西能受到c/c++的静态类型安全检查机制的强制保护,防止意外的修改。
10.strcpy和memcpy的区别
strcpy提供了字符串的复制。即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符。 strcpy函数的原型是:char* strcpy(char* dest, const char* src)
;
strcpy的风险:(strcpy本身没有什么风险,风险来源于传递进去的两个参数)
1、内存不够:strcpy(x,y),字符串y比x大的话,就越界了
2、没有结束符
3、拷贝自身
memcpy提供了一般内存的复制。即memcpy对于需要复制的内容没有限制,因此用途更广。
char *strcpy(char * dest, const char * src) // 实现src到dest的复制{ if ((src == NULL) || (dest == NULL)) { //判断参数src和dest的有效性 return NULL; } char *strdest = dest; //保存目标字符串的首地址 while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下 return strdest;}void *memcpy(void *memTo, const void *memFrom, size_t size){ if ((memTo == NULL) || (memFrom == NULL)) { //memTo和memFrom必须有效 return NULL; } char *tempFrom = (char *)memFrom; //保存memFrom首地址 char *tempTo = (char *)memTo; //保存memTo首地址 while (size -- > 0) { //循环size次,复制memFrom的值到memTo中 *tempTo++ = *tempFrom++ ; } return memTo;}复制代码
strcpy和memcpy主要有以下3方面的区别。
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
11.size_t
size_t类型是一个类型定义,通常将一些无符号的整形定义为size_t,比如说unsigned int或者unsigned long,甚至unsigned long long。每一个标准C实现应该选择足够大的无符号整形来代表该平台上最大可能出现的对象大小。
size_t的定义在<stddef.h>, <stdio.h>,<stdlib.h>,<string.h>,<time.h>和<wchar.h>这些标准C头文件中,也出现在相应的C++头文件, 等等中,在使用size_t之前应该头文件中至少包含一个这样的头文件。
包含以上任何C头文件(由C或C++编译的程序)表明将size_t作为全局关键字。
根据定义,size_t是sizeof关键字(注:sizeof是关键字,并非运算符)运算结果的类型。所以,应当通过适当的方式声明n来完成赋值: n = sizeof(thing);
参数中带有size_t的函数通常会含有局部变量用来对数组的大小或者索引进行计算,在这种情况下,size_t是个不错的选择。适当地使用size_t还会使你的代码变得如同自带文档。当你看到一个对象声明为size_t类型,你马上就知道它代表字节大小或数组索引,而不是错误代码或者是一个普通的算术值。
12.new 和malloc 的区别
相同点:都可用于申请动态内存和释放内存
不同点:
- 申请的内存所在位置不同:new操作符从堆上为对象动态分配内存,malloc函数从自由存储区上为对象动态分配内存。
- 内存分配失败时的返回值不同:new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。 在使用C语言时,我们习惯在malloc分配内存后判断分配是否成功:
- 操作对象有所不同:malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。
- 用法不同:
-
函数malloc 的原型如下:
void * malloc(size_t size)
用malloc 申请一块长度为length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length)
1、malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
2、 malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
-
函数free 的原型如下:
void free( void * memblock )
为什么free 函数不象malloc函数那样复杂呢?这是因为指针p的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free对p 无论操作多少次都不会出问题。如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。
-
运算符new 使用起来要比函数malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length)
int *p2 = new int[length]
这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如:
Obj *objects = new Obj[100]
// 创建100 个动态对象不能写成
Obj *objects = new Obj[100](1)
// 创建100 个动态对象的同时赋初值1 -
在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如:
delete []objects
// 正确的用法delete objects;
// 错误的用法后者相当于delete objects[0],漏掉了另外99 个对象。
-
综上:
1、new自动计算需要分配的空间,而malloc需要手工计算字节数
2、new是类型安全的,而malloc不是,比如:
int* p = new float[2]
// 编译时指出错误
int* p = malloc(2*sizeof(float))
// 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
3、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
4、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
5、malloc/free要库文件支持,new/delete则不要。
13.C++中的new/delete与operator new/operator delete
new operator/delete operator就是new和delete操作符,而operator new/operator delete是函数。
new operator:
(1)调用operator new分配足够的空间,并调用相关对象的构造函数
(2)不可以被重载
operator new:
(1)只分配所要求的空间,不调用相关对象的构造函数。当无法满足所要求分配的空间时,则
->如果有new_handler,则调用new_handler,否则->如果没要求不抛出异常(以nothrow参数表达),则执行bad_alloc异常,否则->返回0复制代码
(2)可以被重载
(3)重载时,返回类型必须声明为void*
(4)重载时,第一个参数类型必须为表达要求分配空间的大小(字节),类型为size_t
(5)重载时,可以带其它参数
14.C++的内存分配
在C++中,内存分成5个区,他们分别是:
-
堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
-
栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
-
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
-
全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C++堆栈中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
-
常量存储区:常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多,取地址修改)
15.堆和栈的区别
-
管理方式不同
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak
-
空间大小不同
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。(这个值可以通过编译器修改)
-
能否产生碎片不同
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构。
-
生长方向不同
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
-
分配方式不同
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
-
分配效率不同
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
16. 构造函数和析构函数可不可以为虚函数,为什么?
构造函数不能为虚函数,而析构函数可以且常常是虚函数。
-
构造函数不能为虚函数
1)从存储空间角度:
虚函数对应一个虚函数表vtable,这个vtable其实是存储在对象的内存空间的。但是,如果构造函数是虚的,就需要通过vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
即vtable是在构造函数调用后才建立,因而构造函数不可能成为虚函数。
2)从使用角度:
虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义,所以构造函数没有必要是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
-
析构函数可以是虚函数,且常常如此
这个就好理解了,因为此时vtable已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。
在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。在类的继承体系中,基类的析构函数不声明为虚函数容易造成内存泄漏。所以如果你设计一定类可能是基类的话,必须要声明其为虚函数。
17.如何限制一个类对象只能在堆(栈)上分配空间
在C++中,类的对象建立分为两种
一种是静态建立,如A a
另一种是动态建立,如A* ptr=new A
这两种方式是有区别的:
静态建立类对象: 是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
动态建立类对象: 是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。
-
只能在堆上分配类对象——就是不能静态建立类对象,即不能直接调用类的构造函数。 当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。
代码如下:
class A { public: A(){} void destory(){ delete this;} private: ~A(){} }; 复制代码
试着使用A a;来建立对象,编译报错,提示析构函数无法访问。这样就只能使用new操作符来建立对象,构造函数是公有的,可以直接调用。类中必须提供一个destory函数,来进行内存空间的释放。类对象使用完成后,必须调用destory函数。
上述方法的缺点:
-
无法解决继承问题。
如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。 因此析构函数不能设为private。
还好C++提供了第三种访问控制,protected。 将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。
-
类的使用很不方便
使用new建立对象,却使用destory函数释放对象,而不是使用delete。 (使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问。这种使用方式比较怪异。)
为了统一,可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。
代码如下,类似于单例模式:
class A { protected: A(){} ~A(){} public: static A* create() { return new A(); } void destory() { delete this; } }; 复制代码
这样,调用create()函数在堆上创建类A对象,调用destory()函数释放内存。
-
-
只能在栈上分配类对象
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。 虽然你不能影响new operator的能力(因为那是C++语言内建的),但是你可以利用一个事实:new operator 总是先调用 operator new,而后者我们是可以自行声明重写的。
因此,将operator new()设为私有即可禁止对象被new在堆上。 代码如下:
class A { private: void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete(void* ptr){} // 重载了new就需要重载delete public: A(){} ~A(){} }; 复制代码
18.拷贝构造函数如果用值传递会有什么影响?
参数为引用,不为值传递是为了防止拷贝构造函数的无限递归,最终导致栈溢出。
如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class)
,那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。
需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass* c_class)
,也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。
19.stl 相关容器,底层用什么实现的?
详细见