面试-;面试题
Posted ssopp24
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试-;面试题相关的知识,希望对你有一定的参考价值。
1.请说出static 和 const关键字尽可能多的作用
static关键字至少有下列n个作用:
1. 函数体内static变量的作用范围为该函数体(变量作用域),链接属性为空链接,生命周期为随程序(静态局部变量),不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
2. 在文件内的static全局变量可以被文件内所有函数访问(作用域),但不能被文件外其它函数访问,链接属性为内链接,生命周期为随程序(静态全局变量);
3. static关键字修饰局部变量时改变变量的生命周期为随程序,不改变变量的作用域,也不改变变量的链接属性(空链接)。 static关键字修饰全局变量时,改变变量的链接属性(由外链接变为内链接),改变作用域为本文件内有效,不改变变量的生命周期;
4. 在文件内的static函数只可被这一文件内的其它函数调用,这个函数的使用范围被限制在声明它的文件内;
5. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
6. 在类中的static成员函数属于整个类所拥有,并不属于某个特定的 类实例化出的对象,所以这个函数不接收this指针,因而只能访问类的static成员变量。
const关键字至少有下列n个作用:
1. 欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
3. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数(const 修饰*this),不能修改类的成员变量;
5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
例如:
const classA operator*( const classA& a1, const classA& a2 );
operator*的返回结果是一个临时对象,它必须是一个const对象。如果不是,这样的变态代码也不会编译出错:
classA a, b, c;
(a * b) = c; // 对a*b的结果赋值
操作(a * b) = c显然不符合编程者的初衷,也没有任何意义。
2.const 与 #define 相比有什么不同?
C++语言可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的优点:
1.const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且字符替换中可能会产生意料不到的错误(边际效应)。
2.有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
C++ 语言可以用const 来定义常量,也可以用#define 来定义常量。但是前者比后者有更多的优点:
(1编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。
(2类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
(3存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次。(宏定义不分配内存但宏定义增加代码段长度)
const常量会分配内存(可以是堆中也可以是栈中)。
(4const 可以节省空间,避免不必要的内存分配。 例如:
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......
double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存!
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以const定义的常量在程序运行过程中只有一份拷贝(因为是全局的只读变量,存在静态区),而#define定义的常量在内存中有若干个拷贝。
(5提高了效率。 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
(6宏替换只作替换,不做计算,不做表达式求解;
宏预编译时就替换了,程序运行时,并不分配内存。
实现机制
宏是预处理命令,即在预编译阶段进行字节替换。const常量是变量,在执行时const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态存储区的只读数据区。根据c/c++语法,当你声明该量为常量,即告诉程序和编译器,你不希望此量被修改。 程序的实现,为了保护常量,特将常量都放在受保护的静态存储区内。凡是试图修改这个区域内的值,都将被视为非法,并报错。 这不能理解为凡是字符串都是放在静态存储区域的。这个跟数据类型没有关系,而是这个量是变量还是常量的问题。例如,一个字符串变量就是可以被修改的。 这种静态存储区域的保护机制是由编译器实现的,而非存储该值的内存的属性。换言之,实质上内存永远都可以被用户随意修改,只是编译器给用户的代码注入了一些自己的保护代码,通过软件手段将这段内存软保护起来。这种保护在汇编级别可以轻松突破,其保护也就无效了。)。
用法区别
define宏定义和const常变量区别:
1.define是宏定义,程序在预处理阶段将用define定义的内容进行了替换。因此程序运行时,常量表中并没有用define定义的常量,系统不为它分配内存。const定义的常量,在程序运行时在常量表中,系统为它分配内存。
2.define定义的常量,预处理时只是直接进行了替换。所以编译时不能进行数据类型检验。const定义的常量,在编译时进行严格的类型检验,可以避免出错。
3.define定义表达式时要注意“边缘效应”,例如如下定义:
#define N 2+3 //我们预想的N值是5,我们这样使用N,int a = N/2; //我们预想的a的值是2,可实际上a的值是3。原因在于在预处理阶段,编译器将 a = N/2处理成了 a = 2+3/2;这就是宏定义的字符串替换的“边缘效应”因此要如下定义:#define N (2+3)。const定义的表达式则没有上述问题。const定义的常量叫做常变量原因有二:const定义常量像变量一样检查类型;const可以在任何地方定义常量,编译器对它的处理过程与变量相似,只是分配内存的地方不同。
3. 内存对齐
(1.概念
对齐跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。比如在32位cpu下,假设一个整型变量的地址为0x00000004,那它就是自然对齐的。
(2.为什么要字节对齐
需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。
(3.正确处理字节对齐
对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
联合 :按其包含的长度最大的数据类型对齐。
结构体: 结构体中每个数据类型都要对齐。
(4.什么时候需要设置对齐
在结构体中,编译器为结构的每个成员按其自然边界分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的”对齐”. 比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除.
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
使用伪指令#pragma pack (),取消自定义字节对齐方式。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要两个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
规则:
对齐原因:
1、平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的。某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2、性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
结构体内存对其规则:
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
//对齐数 = 编译器默认的一个对齐数 与 该成员对齐数大小的较小值
VS中默认的值为8
linux中的默认值为4
3.结构体总大小为各成员最大对齐数(每个成员变量都有1个对齐数)的整数倍
4.如果嵌套了结构体,被嵌套的结构体对齐到自身的最大对齐数的
整数倍处,结构体的整体大小就是所有成员中(含嵌套结构体)最大对齐数的整数倍
4.说明sizeof和strlen之间的区别
1.sizeof操作符的返回类型是size_t,它是在头文件中 typedef的unsigned int类型。该类型保证其大小足以存储内存中对象的大小
2.sizeof是运算符,strlen是函数
3.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以'\\0'结尾的。sizeof还可以用函数返回值做参数-->sizeof(F( ))(对函数使用sizeof,在编译阶段会被函数返回值的类型取代)
4.数组做sizeof参数不退化,数组传递给strlen退化为指针
5.大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因
6.strlen的结果是在运行时计算的,用来计算字符串的长度,而不是占内存的大小
7.sizeof后面是类型要加括号,变量名可以不加括号。因为sizeof是操作符而不是函数
8.当使用结构类型或变量时,sizeof返回实际的大小。当使用静态的数组时,sizeof返回全部数组的大小。sizeof操作符不能返回动态分配的数组(指针)或外部的数组(参数传进来)的大小
9.sizeof操作符不能用于函数类型、不完全类型或位字段。不完全类型指具有未知存储大小数据的数据类型,如未知存储大小的数组类型,未知内容的结构或联合类型、void类型等
数组作为参数传递给函数退化为指针(数组首地址)。在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。如果想在函数内知道数组的大小,需要将数组长度由另一个形参传进去
5.说明sizeof的使用场合
(1.sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。例如:
void *malloc(size_t size),
size_t fread(void * ptr, size_t size, size_t nmemb, FILE * stream)
(2.用它可以看某种类型的对象在内存中所占的单元字节。例如:
void * memset(void * s, int c, sizeof(s))
(3.在动态分配一对象时,可以让系统知道要分配多少内存
(4.由于操作数的字节数在实现时可能出现变化,在涉及操作数字节大小时最好用sizeof代替常量计算
(5.如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小
6.空类所占大小为1,单继承的空类大小也是1,多继承的空类大小还是1.
注意不要说类的大小,是类的对象的大小。
首先,类的大小是什么?确切的说,类只是一个类型定义,它是没有大小可言的。 用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。
一个对象的大小大于等于所有非静态成员大小的总和。
为什么是大于等于而不是正好相等呢?超出的部分主要有以下两方面:
1) C++对象模型本身 对于具有虚函数的类型来说,需要有一个方法为它的实体提供类型信息(RTTI)和虚函数入口,常见的方法是建立一个虚函数入口表,这个表可为相同类型的对象共享,因此对象中需要有一个指向虚函数表的指针,此外,为了支持RTTI,许多编译器都把该类型信息放在虚函数表中。但是,是否必须采用这种实现方法,C++标准没有规定,但是这几户是主流编译器均采用的一种方案。
2) 编译器优化 因为对于大多数CPU来说,CPU字长的整数倍操作起来更快,因此对于这些成员加起来如果不够这个整数倍,有可能编译器会插入多余的内容凑足这个整数倍,此外,有时候相邻的成员之间也有可能因为这个目的被插入空白,这个叫做“补齐”(padding)。所以,C++标准仅仅规定成员的排列按照类定义的顺序,但是不要求在存储器中是紧密排列的。
基于上述两点,可以说用sizeof对类名操作,得到的结果是该类的对象在存储器中所占据的字节大小,由于静态成员变量不在对象中存储,因此这个结果等于各非静态数据成员(不包括成员函数)的总和加上编译器额外增加的字节。后者依赖于不同的编译器实现,C++标准对此不做任何保证。
C++标准规定类的大小不为0,空类的大小为1,当类不包含虚函数和非静态数据成员时,其对象大小也为1。 如果在类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针指向虚函数表VTable,在32位机器上,一个对象会增加4个字节来存储此指针,它是实现面向对象中多态的关键。而虚函数本身和其他成员函数一样,是不占用对象的空间的。
一个类中,虚函数、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的。
对象大小= vptr(可能不止一个,这个很难确定,不过试过,类中定义了一个virtual函数,仍然为占用4个字节) + 所有非静态数据成员大小 + Aligin字节大小(依赖于不同的编译器)
c++空类实例大小不是0原因?
首先:我们要知道什么是类的实例化,所谓类的实例化就是在内存中分配一块地址.
类a,b明明是空类,它的大小应该为为0,为什么编译器输出的结果为1呢?这就是我们刚才所说的实例化的原因(空类同样可以被实例化),每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以a,b的大小为1.
而类c是由类a派生而来,它里面有一个纯虚函数,由于有虚函数的原因,有一个指向虚函数的指针(vptr),在32位的系统分配给指针的大小为4个字节,所以最后得到c类的大小为4.
类d的大小更让初学者疑惑吧,类d是由类b,c派生迩来的,它的大小应该为二者之和5,为什么却是8呢?这是因为为了提高实例在内存中的存取效率.类的大小往往被调整到系统的整数倍.
当然在不同的编译器上得到的结果可能不同,但是这个实验告诉我们初学者,不管类是否为空类,均可被实例化(空类也可被实例化),每个被实例都有一个独一无二的地址.
为什么类b多了一个数据成员,却大小和类a的大小相同呢?因为:类b的静态数据成员被编译器放在程序的一个global data members中,它是类的一个数据成员.但是它不影响类的大小,不管这个类实际产生了多少实例,还是派生了多少新的类,静态成员数据在类中永远只有一个实体存在,而类的非静态数据成员只有被实例化的时候,它们才存在.但是类的静态数据成员一旦被声明,无论类是否被实例化,它都已存在.可以这么说,类的静态数据成员是一种特殊的全局变量.
出类的大小与它当中的构造函数,析构函数,以及其他的成员函数无关,只与它当中的成员数据有关.
1.为类的非静态成员数据的类型大小之和.
2.由编译器额外加入的成员变量的大小,用来支持语言的某些特性(如:指向虚函数的指针).
3.为了优化存取效率,进行的边缘调整(字节对齐).
4 与类中的构造函数,析构函数以及其他的成员函数无关.
C++的空类是指这个类不带任何数据,即类中没有非静态(non-static)数据成员变量,没有虚函数(virtual function),也没有虚基类(virtual base class)。
直观地看,空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。换句话说,
C++空类的大小不为0
class NoMembers
;
NoMembers n;
sizeof(n) == 1
C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。这是由于:
new需要分配不同的内存地址,不能分配内存大小为0的空间
避免除以 sizeof(T)时得到除以0错误
故使用一个字节来区分空类。
值得注意的是,这并不代表一个空的基类也需要加一个字节到子类中去。这种情况下,空类并不是独立的,它附属于子类。子类继承空类后,子类如果有自己的数据成员,而空基类的一个字节并不会加到子类中去。例如,
class Empty ;
struct D : public Empty int a;;
sizeof(D)为4。
再来看另一种情况,一个类包含一个空类对象数据成员。class Empty ;
class HoldsAnInt
int x;
Empty e;
;
在大多数编译器中,你会发现 sizeof(HoldsAnInt) 输出为8。这是由于,Empty类的大小虽然为1,然而为了内存对齐,编译器会为HoldsAnInt额外加上一些字节,使得HoldsAnInt被放大到足够又可以存放一个int。
7.内联函数和宏的差别是什么?
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换。
内联函数要做参数类型检查,这是内联函数跟宏相比的优势。
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到哪里去,
对于短小的代码来说inline增加了空间消耗换来的是效率提高,这方面和宏是一模一样的,但是在inline在和宏相比没有付出任何额外代价的情况下更安全。至于需不需要inline,就要根据实际情况来取舍了。
inline一般只用于如下情况:
a:一个函数不断被重复调用
b:函数只有简单的几行,且函数内不包含for、while、switch语句
一般来说,我们写小程序没有必要定义inline,但是如果要完成一个工程项目,当一个简单的函数被调用多次时,应该考虑用inline。
宏在C语言里极其重要,而在C++里用得就少多了。
关于宏的第一规则是绝不应该去使用它,除非你不得不这样做。几乎每个宏都表明了程序设计语言里、程序里或者程序员的一个缺陷。因为它将在编译器看到程序正文之前重新摆布这些正文。宏也是许多程序设计主要麻烦。所以,如果你使用了宏,就应该准备只能从各种工具中得到较少的服务。
宏是在代码出不加任何验证的简单替换。而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
宏不是函数,只是在编译前(编译预处理阶段)将程序有关字符串替换成宏
关键字inlin必须与函数定义体放在一起才能使函数成为内联函数,仅将inlin放在函数声明前面不起任何作用。
如下面代码:函数FOO不能成为内联函数:
inline void Foo(int x, int y);//inline 仅在函数声明放在一起
void Foo(int x, int y)
而如下风格函数Foo则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y)//inline与函数定义放在一起。
inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。 内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。 如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。 另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
1 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。 类的构造函数和析构函数容易让人误解成使用内联更有效。 要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。 所以不要随便地将构造函数和析构函数的定义体放在类声明中。 一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。
宏定义和内联函数的区别
1. 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。
内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
2. 宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换
内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
3. 宏定义是没有类型检查的,无论对还是错都是直接替换
内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
4. 宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段在所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率
内联函数和普通函数的区别
1. 内联函数和普通函数的参数传递机制相同,但是编译器会在每处调用内联函数的地方将内联函数内容展开,这样既避免了函数调用的开销又没有宏机制的缺陷
2. 普通函数在被调用的时候,系统首先要到函数的入口地址去执行函数体,执行完成之后再回到函数调用的地方继续执行,函数始终只有一个复制。
内联函数不需要寻址,当执行到内联函数的时候,将此函数展开,如果程序中有N次调用了内联函数则会有N次展开函数代码
3. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句。如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行
8.指针和引用的差别
(1 非空区别。在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这是你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。
(2 合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
(3 可修改区别。指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
(4 应用区别。总的来说,在以下情况下你应该使用指针:一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
指针:指针是一个变量,这个变量存储的是一个地址,指向内存的一个存储单元间接对对象进行读写;而引用跟原来的变量实质上是同一个东西,是原变量的一个别名。
引用和指针的区别和联系:
1.引用只能在定义时初始化一次,并且引用被创建的同时必须被初始化,之后不能改变指向其它变量(从一而终,不能改变引用关系);指针则可以在任何时候被初始化,可以随时改变所指的对象。
2.引用必须指向有效的变量,不能有 NULL 引用,引用必须与合法的存储单元关联,指针可以为空
3.sizeof 指针对象 和 引用对象的意义不一样。 sizeof引用得到的是所指向的变量的大小,而sizeof指针是指针的大小。
4.指针和引用自增(++)和自减(--)意义不一样。
5.相对而言,引用比指针更安全。
总结一下:指针比引用更灵活,但是也更危险。 使用指针时一定要注意检查指针是否为空。指针所指的地址释放以后指针最好置NULL,否则可能存在野指针的问题。
8.指针函数和函数指针?
http://blog.csdn.net/ameyume/article/details/8220832
9.指针数组和数组指针?
1.什么是指针数组和数组指针?
指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的地址,或者说这个指针指向一个数组。
根据上面的解释,可以了解到指针数组和数组指针的区别,因为二者根本就是两种类型的变量。
2.指针数组和数组指针到底是什么?
2.1指针数组
首先先定义一个指针数组,既然是数组,名字就叫arr
char* arr[4] = "hello", "world", "shannxi", "xian";
// arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char* 类型的指针,这些指针存放着其对应字符串的首地址。
// 定义时如果记不清优先级可以通过加括号解决: 比如定义一个指针数组,可以写成char* (arr[4])。不过在定义之前一定要清楚自己定义的变量,如果目的是一个数组,那就把arr[4]括起来,如果是一个指针,就把*arr括起来。
// 如果是看到一段这样的代码,可以从它的初始化来分辨它是数组还是指针,很明显,我这定义的是一个数组,如果是指针,会用NULL来初始化。
内存映像图 内容权限
栈区 函数中的普通变量 可读可写
堆区 动态申请的内存 可读可写
静态变量区 static修饰的变量 可读可写
数据区 用于初始化变量的常量 只读
代码区 代码指令 只读
一般情况下,从栈区到代码区,是从高地址到低地址。栈向下增长,堆向上增长。
arr[4]是一个在主函数定义的数组。把它对应到对应到内存中,arr是一个在栈区,有四个元素的数组,而每一个数组又是一个指针.
arr的“hello”, “world”, “shannxi”, “xian”也存在内存中,只是跟arr这个变量不在同一段空间,它们被分配在只读数据区,数组arr[4]的四个指针元素,分别存放着这四个字符串的首地址,想象一下,从栈区有四只无形的手指向数据区的空间。arr+1会跳过四个字节。也就是一个指针的大小
这就相当于定义char *p1 = “hello”,char *p1 = “world”,char *p3 = “shannxi”, char *p4 = “xian”,这是四个指针,每个指针存放一个字符串首地址,然后用arr[4]这个数组分别存放这四个指针,就形成了指针数组。
2.2数组指针
首先来定义一个数组指针,既然是指针,名字就叫pa
char (*pa)[4];
如果指针数组和数组指针这俩个变量名称一样就会是这样:char *pa[4]和char (*pa)[4],原来指针数组和数组指针的形成的根本原因就是运算符的优先级问题,所以定义变量是一定要注意这个问题,否则定义变量会有根本性差别!
pa是一个指针指向一个char[4]的数组,每个数组元素是一个char类型的变量,所以我们不妨可以写成:char[4] (*pa);这样就可以直观的看出pa的指向的类型,不过在编辑器中不要这么写,因为编译器根本不认识,这样写只是帮助我们理解。
既然pa是一个指针,存放一个数组的地址,那么在我们定义一个数组时,数组名称就是这个数组的首地址,那么这二者有什么区别和联系呢?
char a[4];,
a是一个长度为4的字符数组,a是这个数组的首元素地址。既然a是地址,pa是指向数组的指针,那么能将a赋值给pa吗?答案是不行的!因为a是数组首元素地址,pa存放的却是数组首地址,a是char 类型,a+1,a的值会实实在在的加1,而pa是char[4]类型的,pa+1,pa则会加4,虽然数组的地址和首元素地址的值相同,但是两者操作不同,所以类型不匹配不能直接赋值,但是可以这样:pa = &a,pa相当与二维数组的行指针,现在它指向a[4]的地址。
数组名代表数组首元素地址
3.指针数组和数组指针的使用
3.1指针数组在参数传递时的使用
指针数组常用在主函数传参,在写主函数时,参数有两个,一个确定参数个数,一个指针数组用来接收每个参数(字符串)的地址
int main(int argc, char *argv[])
此时可以想象内存映像图,主函数的栈区有一个叫argv的数组,这个数组的元素是你输入的参数的地址,指向着只读数据区。
如果是向子函数传参,这和传递一个普通数组的思想一样,不能传递整个数组过去,如果数组很大,这样内存利用率很低,所以应该传递数组首元素的地址,用一个指针接收这个地址。因此,指针数组对应着二级指针
void fun(char** pp);//子函数中的形参
fun(p/*p是一个指针数组的数组名*/);//主函数中的实参
数组指针传参时的使用
数组指针既然是一个指针,那么就是用来接收地址,在传参时就接收数组的地址,所以数组指针对应的是二维数组。
void fun(int (*P)[4]);//子函数中的形参,数组指针
a[3][4] = 0;//主函数中定义的二维数组
fun(a);//主函数调用子函数的实参,是二维数组的首元素地址(一维数组的地址)(所以形参为数组指针,指向数组的指针)
//指针数组 数组名做实参,数组名代表首元素地址,即指针的地址,所以形参为二级指针
//二维数组 数组名做实参,数组名代表首元素地址,即一维数组的地址,所以用数组指针,指向数组的指针
10.空指针与野指针的区别是什么?
当delete一个指针的时候,实际上仅是让编译器释放内存,但指针本身依然存在。这时它就是一个野指针。
当使用以下语句时,可以把野指针改为空指针:
myPtr = NULL;
通常,如果在删除一个指针后又把它删除一次,程序就会变得非常不稳定,任何情况都有可能发生。但是如果你只是删除了一个空指针,则什么事都不会发生,这样做非常安全。
使用野指针或空指针(如果myPtr=0)是非法的,而且有可能造成程序崩溃。如果指针是空指针,尽管同样是崩溃,但它同野指针的崩溃相比是一种可预料的崩溃。这样调试起来会方便得多。
野指针指一个指针变量指向了不可使用的内存空间。
野指针不是 NULL 指针,是指向“垃圾”内存的指针。人们一般不会错用 NULL指针,因为用 if 语句很容易判断。但是“野指针”是很危险的, 因为if 语句对它不起作用。
产生野指针三个原因:
(1 指针变量创建时候没有被初始化:任何指针变量在创建的时候不会自动成为NULL指针,它的默认值是随机的,它会乱指一气。所以指针变量在创建的同时应当被初始化,要么将指针设置为 NULL,要么让它指向合法的内存。否则该指针就会成为一个野指针,可能指向一块不可使用的内存空间。
例如char *p; 这样创建一个指针p,指向一个随机的内存地址空间
所以指针在创建的时候要被初始化,可以将其初始化为NULL,或指向合法的内存空间
比如 char *p = NULL ; 或 char *p = new char; //这个时候p就不会是一个野指针
(2 delete或free指针之后没有把指针设置为NULL:delete和free只是把指针所指的内存空间释放掉,而没有对指针本身进行释放,并没有清理掉指针。这时候的指针依然指向原来的位置,只不过这个位置的内存数据已经被毁尸灭迹,此时的这个指针指向的内存就是一个垃圾内存。但是此时的指针由于并不是一个NULL指针(在没有置为NULL的前提下),指针没有置为 NULL,让人误以为是个合法的指针。在做如下指针校验的时候
if(p != NULL)
会逃过校验,此时的p不是一个NULL指针,也不指向一个合法的内存块,造成程序中指针访问失败。
比如char *p = new char[4] ; delete[] p; //这时候指针p所指的内存空间被释放,但是指针p本身不为空,指针p所指向的内存空间已经不能使用,造成了野指针。正确的做法是及时的把指针p赋值为NULL
(3 指针操作超过了指向内存空间的作用范围(指针操作超越了变量的作用范围):当指针越界之后也会变成一个野指针。指针指向一个临时变量的引用,当该变量被释放时,此时的指针就变成了一个野指针
由于C/C++中指针有++操作,因而在执行该操作的时候,稍有不慎,就容易指针访问越界,访问了一个不该访问的内存,结果程序崩溃
如:
class A
public:
void Func( ) cout << “Func of class A” << endl;
;
void Test( )
A* p;
A a;
p = &a; // 注意 a 的生命期
p->Func(); // p 是“野指针”
函数 Test 在执行语句 p->Func()时,对象 a 已经消失,而 p 是指向 a 的,所以 p 就成了“野指针”。
无类型指针
无类型指针指的是void* 这种指针,表示可以指向任何数据类型。
比如
int n = 3;
int *p = NULL; //说明指针p此时空闲,没有指向任何有意义的内存空间
void *gp = &n; //无类型指针gp指向整型变量n
p = (int *)gp; //把无类型指针转换为整型指针
printf("%d\\n", *p);
结果输出3,说明无类型指针可以转换成任何数据类型的指针。
空指针常量
一个表示0值的整数常量,叫做空指针常量。例如:0、0L、1-1(它们都是值为0的整数常量表达式)以及(void*)0、void* NULL 都是空指针常量,空指针常量可以赋值给任何指针类型,因为它是变体类型(void*)。但是我们更倾向于使用NULL表示这个空指针常量。对于其它方式(比如0)来表示空指针常量虽然不会产生任何问题,但是在根本意义上并不符合空指针常量的定义。因为空指针常量的存在意义还在强调它并不指向任何对象。
空指针
空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。
空指针是一个特殊的指针,表示当前这个指针变量处于空闲状态,没有指向任何有意义的内存空间,因为这个指针不指向任何地方。这意味任何一个有效的指针如果和空指针进行相等的比较运算时,结果都是false。
在程序中,得到一个空指针最直接的方法就是运用预定义的NULL,这个值在多个头文件中都有定义。
如果要初始化一个空指针,我们可以这样,
int *ip = NULL;
校验一个指针是否为一个有效指针时,我们更倾向于使用这种方式
if(ip != NULL)
而不是
if(ip)
为什么有人会用if(ip)这种方式校验一个指针非空,而且在C++中不会出现错误呢?而且现在很多人都会这样写。
原因是这样的,
// Define NULL pointer value
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif // NULL
在现在的C/C++中定义的NULL即为0,而C++中的true为≠0,所以此时的if(ip)和if(ip != NULL)是等效的。
NULL指针
NULL是一个标准规定的宏定义,用来表示空指针常量。在C++里面NULL被直接定义成了整数0,而在没有__cplusplus定义的前提下,就被定义成一个值是0的 void* 类型的指针常量
零指针
零值指针,是值为0的指针,可以是任何一种类型的指针,可以是通用变体类型 void*,也可以是 char*, int* 等等。
在C++里面,任何一个概念都以一种语言内存公认的形式表现出来,例如std::vector会提供一个empty()子函数来返回容器是否为空,然而对于一个基本数值类型(或者说只是一个类似整数类型的类型)我们不可能将其抽象成一个类(当然除了auto_ptr等智能指针)来提供其详细的状态说明,所以我们需要一个特殊值来做为这种状态的表现。
C++标准规定,当一个指针类型的数值是0时,认为这个指针是空的。(我们在其它的标准下或许可以使用其它的特殊值来定义我们需要的NULL实现,可以是1,可以是2,是随实现要求而定的,但是在标准C++下面我们用0来实现NULL指针)
空指针指向内存的什么地方
标准并没有对空指针指向内存中的什么地方这一问题作出规定,也就是说用哪个具体地址值表示空指针取决于系统实现。我们常见的空指针一般指向0地址,即空指针的内部用全0来表示(zero null pointer,零空指针);也有一些系统用一些特殊的地址值或者特殊的方式表示空指针(nonzero null pointer,非零空指针)
在编程中不需要了解在我们的系统上空指针到底是一个zero null pointer还是 nonzero null pointer,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽其中的实现细节。注意:不要把空指针的内部实现表示等同于整数0的对象表示——如上所述,有时它们是不同的。
对空指针实现的保护政策
逻辑地址和物理地址
既然我们选择了0作为空的概念。在非法访问空的时候我们需要保护以及报错。因此,编译器和系统提供了很好的政策。
我们程序中的指针其实是windows内存段偏移后的地址,而不是实际的物理地址,所以不同的地址中的零值指针指向的同一个0地址,其实在内存中都不是物理内存的开端的0,而是分段内存的开端,这里我们需要简单介绍一下windows下的内存分配和管理制度:
windows下,执行文件(PE文件)在被调用后,系统会分配给它一个额定大小的内存段用于映射这个程序的所有内容(就是磁盘上的内容)并且为这个段进行新的偏移计算,也就是说我们的程序中访问的所有near指针都是在我们“自家”的段里面的,当我们需要访问far指针的时候,我们其实是跳出了“自家的院子”到了他人的地方,我们需要一个段偏移资质来完成新的偏移(人家家里的偏移)所以我们的指针可能是OE02:0045就是告诉我们要访问0E02个内存段的0045号偏移,然后windows会自动给我们找到0E02段的开始偏移,然后为我们计算真实的物理地址。
所以程序A中的零值指针和程序B中的零值指针指向的地方可能是完全不同的。
空指针赋值分区
这一分区是进程的地址空间中从0x00000000 到 0x0000FFFF 的闭区间(64K 的内存大小),这 64K 的内存是一块保留内存,不能被程序动态内存分配器分配,不能访问,也不能使用,保留该分区的目的是为了帮助程序员捕获对空指针的赋值。如果进程中的线程试图读取或者写入位于这一分区内的内存地址,就会引发访问违规。
为什么空指针访问会出现异常
归根结底,程序中所使用的数据都需要从物理设备上获取,即程序中的数据需要从一个真实的物理地址中读取或者写入。所以当一个指针的逻辑地址可以通过计算能够准确无误的映射到一个正确的物理地址上时,这时候数据的访问就是正确的,程序的执行也没有任何问题。如果一个指针为空指针,那么该指针所指向的逻辑地址空间位于空指针赋值分区的区间上。空指针赋值分区上的逻辑地址没有物理存储器与之对应,因而访问时就会产生违规访问的异常。
11.C++中有了malloc/free,为什么还需要new/delete?
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理和释放内存工作的运算符delete。new/delete不是库函数,而是运算符。
malloc 是C语言的标准库函数,它实现了在堆内存管理中进行按需分配的机制,但是它不提供C++中对像构造的支持,而new 则是一个 在C++中同时完成堆内存按需分配支持和对像构造功能的运算符,malloc只能分配动态内存,而new除了分配动态内存还能构造对象,free只能释放内存,而delete除了释放内存还能执行析构函数
我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。
12.this指针
关于this指针,有这样一段描述:当你进入一个房子后,你可以看见桌子、椅子、地板等,但房子的全貌你是看不到的。
对于一个实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this指针就是这样一个指针,它时时刻刻指向这个实例本身。
this指针易混的几个问题如下:
(1. this 指针本质是一个函数参数,只是编译器隐藏起来,形式的、语法层面上的参数。this只能在成员函数中使用,全局函数,静态函数都不能使用this。实际上,成员函数默认第一个参数为T* const this。如:
class A
public:
int func(int p)
;
其中,func的原型在编译器看来应该是:
int func(A* const this, int p);
(2. this在成员函数的开始前构造,在成员的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类成员函数时,编译器将类的指针作为函数的this参数传递进去。如:
A a;
a.func(10);
此处,编译器将会编译成:
A::func(&a, 10);
看起来和静态函数没差别,不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,如VC通常是通过ecx寄存器传递this参数的。
(3. this指针并不占用对象的空间。 this相当于非静态成员函数的一个隐含的参数,不占对象空间。它跟对象之间没有包含关系,只是当前调用函数的对象被它指向而已。所有成员函数的参数,不管是不是隐含的,都不会占用对象的空间,只会占用参数传递时的栈空间,或者直接占用一个寄存器。
(4. this 指针是什么时候创建的?
this 在成员函数的开始执行前构造,在成员的执行结束后清除。 但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当作C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new方式创建对象的话,在堆里分配内存,new操作符通过eax返回分配地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx。
(5. this指针存放在何处?堆、栈、还是其他?
this指针会因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。
语法上,this是个指向对象的“常指针”,因此无法改变。它是一个指向相应对象的指针。所有对象共用的成员函数利用这个指针区别不同变量,也就是说,this是“不同对象共享相同成员函数”的保证。
而在实际应用的时候,this应该是个寄存器参数。这个不是语言规定的,而是“调用约定”,C++默认调用约定是_cdecl,也就是c风格的调用约定。该约定规定参数自右向左入栈,由调用方负责平衡堆栈。对于成员函数,将对象的指针(即this指针)存入ecx中。因为这只是一个调用约定,不是语言的组成部分,不同编译器自然可以自由发挥。但是现在的主流编译器都是这么做的。
(6. this指针是如何传递给类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数”的?
大多数编译器通过ecx寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵循一致的传参规则,否则不同编译器产生的obj就无法匹配了。
(7. 我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?
this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
a.this 指针是C++中的一个关键字,也是一个const指针,它指向当前对象,通过它可以访问当前对象的所有成员。
void Student :: setNam(char*name)
this ->name = name;
this虽然用在类的内部,但是只有在对象被创建以后才会给this赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显示的给this赋值。
b.this是const指针,他的值是不能被修改的,一切企图修改该指针的操作,如赋值,递增,递减等都是不允许的
c.this只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
d.只有对象被创建后this才有意义,因此不能再static成员函数中使用
e.this实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给this。不过this这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中
f.this作为隐式形参,本质上是成员函数的局部变量所以只能在成员函数内部使用,并且只有在通过对象调用成员函数时才给this赋值。
13.头文件中的 ifndef/define/endif是干什么用的?
头文件中的 ifndef/define/endif是条件编译的一种,除了防止头文件被重复引用(整体),还可以防止重复定义(变量、宏或结构)
以上是关于面试-;面试题的主要内容,如果未能解决你的问题,请参考以下文章