C++⾯试常见问题
指针和引⽤的区别
指针是⼀个变量,存储的是⼀个地址,引⽤跟原来的变量实质上是同⼀个东西,是原变量的别名
指针可以有多级,引⽤只有⼀级
指针可以为空,引⽤不能为NULL且在定义时必须初始化
指针在初始化后可以改变指向,⽽引⽤在初始化之后不可再改变
sizeof指针得到的是本指针的⼤⼩,sizeof引⽤得到的是引⽤所指向变量的⼤⼩
当把指针作为参数进⾏传递时,也是将实参的⼀个拷贝传递给形参,两者指向的地址相同,但不是同⼀个变量,在函数中改变这个变量的指向不影响实参,⽽引⽤却可以。
引⽤只是别名,不占⽤具体存储空间,只有声明没有定义;指针是具体变量,需要占⽤存储空间。
引⽤在声明时必须初始化为另⼀变量,⼀旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量⽽不初始化,等⽤到时再指向具体变量。
引⽤⼀旦初始化之后就不可以再改变(变量可以被引⽤为多次,但引⽤只能作为⼀个变量引⽤);指针变量可以重新指向别的变量。
不存在指向空值的引⽤,必须有具体实体;但是存在指向空值的指针。
堆和栈的区别
申请⽅式不同:栈由系统⾃动分配;堆是⾃⼰申请和释放的。
申请⼤⼩限制不同:栈顶和栈底是之前预设好的,栈是向栈底扩展,⼤⼩固定,可以通过ulimit -a查看,由ulimit -s修改;堆向⾼地址扩展,是不连续的内存区域,⼤⼩可以灵活调整。
申请效率不同:栈由系统分配,速度快,不会有碎⽚;堆由程序员分配,速度慢,且会有碎⽚。
区别以下指针类型
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
int *p[10]表⽰指针数组,强调数组概念,是⼀个数组变量,数组⼤⼩为10,数组内每个元素都是指向int类型的指针变量。
int (*p)[10]表⽰数组指针,强调是指针,只有⼀个变量,是指针类型,不过指向的是⼀个int类型的数组,这个数组⼤⼩是10。
int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
new / delete 与 malloc / free的异同
相同点
都可⽤于内存的动态申请和释放
不同点
前者是C++运算符,后者是C/C++语⾔标准库函数
new⾃动计算要分配的空间⼤⼩,malloc需要⼿⼯计算
new是类型安全的,malloc不是。例如:
int *p = new float[2]; //编译错误
int *p = (int*)malloc(2 * sizeof(double));//编译⽆错误
new调⽤名为operator new的标准库函数分配⾜够空间并调⽤相关对象的构造函数,delete对指针所指对象运⾏适当的析构函数;然后通过调⽤名为operator delete的标准库函数释放该对象所⽤内存。后者均没有相关调⽤
后者需要库⽂件⽀持,前者不⽤
new是封装了malloc,直接free不会报错,但是这只是释放内存,⽽不会析构对象
宏定义和typedef区别?
宏主要⽤于定义常量及书写复杂的内容;typedef主要⽤于定义类型别名。
宏替换发⽣在编译阶段之前,属于⽂本插⼊替换;typedef是编译的⼀部分。
宏不检查类型;typedef会检查数据类型。
宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
注意对指针的操作,typedef char * p_char和#define p_char char *区别巨⼤。
strlen和sizeof区别?
sizeof是运算符,并不是函数,结果在编译时得到⽽⾮运⾏中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。
因为sizeof值在编译时确定,所以不能⽤来得到动态分配(运⾏时分配)存储空间的⼤⼩。
int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是8
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。⼤⼩是4
return 0;
}
常量指针和指针常量区别?
常量指针是⼀个指针,读成常量的指针,指向⼀个只读变量。如int const *p或const int *p。
指针常量是⼀个不能给改变指向的指针。指针是个常量,不能中途改变指向,如int *const p。
a和&a有什么区别?
假设数组int a[10];
int (*p)[10] = &a;
a是数组名,是数组⾸元素地址,+1表⽰地址值加上⼀个int类型的⼤⼩,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
&a是数组的指针,其类型为int (*)[10](就是前⾯提到的数组指针),其加1时,系统会认为是数组⾸地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后⼀个元素的地址。
若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引⽤时按照int类型⼤⼩来读取。
C++中struct和class的区别
相同点
两者都拥有成员函数、公有和私有部分
任何可以使⽤class完成的⼯作,同样可以使⽤struct完成
不同点
两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
class默认是private继承,⽽struct模式是public继承
class可以作为模板类型,struct不⾏
引申:C++和C的struct区别
C语⾔中:struct是⽤户⾃定义数据类型(UDT);C++中struct是抽象数据类型(ADT),⽀持成员函数的定义,(C++中的struct能继承,能实现多态)
C中struct是没有权限的设置的,且struct中只能是⼀些变量的集合体,可以封装数据却不可以隐藏数据,⽽且成员不可以是函数
C++中,struct增加了访问权限,且可以和类⼀样有成员函数,成员默认访问说明符为public(为了与C兼容)
struct作为类的⼀种特例是⽤来⾃定义数据结构的。⼀个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名(除:typedef struct class{};);C++中结构体标记(结构体名)可以直接作为结构体类型名使⽤,此外结构体struct在C++中被当作类的⼀种特例
define宏定义和const的区别
编译阶段
define是在编译的预处理阶段起作⽤,⽽const是在编译、运⾏的时候起作⽤
安全性
define只做替换,不做类型检查和计算,也不求解,容易产⽣错误,⼀般最好加上⼀个⼤括号包含住全部的内容,要不然很容易出错
const常量有数据类型,编译器可以对其进⾏类型安全检查
内存占⽤
define只是将宏名称进⾏替换,在内存中会产⽣多分相同的备份。const在程序运⾏中只有⼀份备份,且可以执⾏常量折叠,能将复杂的的表达式计算出结果放⼊常量表
宏替换发⽣在编译阶段之前,属于⽂本插⼊替换;const作⽤发⽣于编译过程中。
宏不检查类型;const会检查数据类型。
宏定义的数据没有分配内存空间,只是插⼊替换掉;const定义的变量只是值不能改变,但要分配内存空间。
C++的顶层const和底层const
概念区分
顶层const:指的是const修饰的变量本⾝是⼀个常量,⽆法修改,指的是指针,就是 * 号的右边
底层const:指的是const修饰的变量所指向的对象是⼀个常量,指的是所指变量,就是 * 号的左边
举个例⼦
int a = 10;
int* const b1 = &a; //顶层const,b1本⾝是⼀个常量
const int* b2 = &a; //底层const,b2本⾝可变,所指的对象是常量
const int b3 = 20; //顶层const,b3是常量不可变
const int* const b4 = &a; //前⼀个const为底层,后⼀个为顶层,b4不可变
const int& b5 = a; //⽤于声明引⽤变量,都是底层const
区分作⽤
执⾏对象拷贝时有限制,常量的底层const不能赋值给⾮常量的底层const
使⽤命名的强制类型转换函数const_cast时,只能改变运算对象的底层const
拷贝初始化和直接初始化
当⽤于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调⽤与实参匹配的构造函数,拷贝初始化总是调⽤拷贝构造函数。拷贝初始化⾸先使⽤指定构造函数创建⼀个临时对象,然后⽤拷贝构造函数将那个临时对象拷贝到正在创建的对象。举例如下
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调⽤构造函数对str2进⾏初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使⽤拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这⾥相当于隐式调⽤拷贝构造函数,⽽不是调⽤赋值运算符函数
为了提⾼效率,允许编译器==跳过创建临时对象这⼀步,直接调⽤构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价)。但是需要辨别两种情况。
当拷贝构造函数为private时:语句3和语句4在编译时会报错
使⽤explicit修饰构造函数时:如果构造函数存在隐式转换,编译时会报错
内联函数和宏定义的区别
内联(inline)函数和普通函数相⽐可以加快程序运⾏的速度,因为不需要中断调⽤,在编译的时候内联函数可以直接嵌⼊到⽬标代码中。
内联函数适⽤场景
使⽤宏定义的地⽅都可以使⽤inline函数
作为类成员接⼝函数来读写类的私有成员或者保护成员,会提⾼效率
为什么不能把所有的函数写成内联函数
内联函数以代码复杂为代价,它以省去函数调⽤的开销来提⾼执⾏效率。所以⼀⽅⾯如果内联函数体内代码执⾏时间相⽐函数调⽤开销较⼤,则没有太⼤的意义;另⼀⽅⾯每⼀处内联函数的调⽤都要复制代码,消耗更多的内存空间,因此以下情况不宜使⽤内联函数:
sizeof 指针函数体内的代码⽐较长,将导致内存消耗代价
函数体内有循环,函数执⾏时间要⽐函数调⽤开销⼤
主要区别
内联函数在编译时展开,宏在预编译时展开
内联函数直接嵌⼊到⽬标代码中,宏是简单的做⽂本替换
内联函数有类型检测、语法判断等功能,⽽宏没有
内联函数是函数,宏不是
宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产⽣歧义
内联函数代码是被放到符号表中,使⽤时像宏⼀样展开,没有调⽤的开销,效率很⾼;
在使⽤时,宏只做简单字符串替换(编译前)。⽽内联函数可以进⾏参数类型检查(编译时),且具有返回值。
内联函数本⾝是函数,强调函数特性,具有重载等功能。
内联函数可以作为某个类的成员函数,这样可以使⽤类的保护成员和私有成员,进⽽提升效率。⽽当⼀个表达式涉及到类保护成员或私有成员时,宏就不能实现了。
构造函数、析构函数、虚函数可否声明为内联函数
⾸先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register⼀样,只是个建议,编译器并不⼀定真正的内联。
register关键字:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,⽽不是通过内存寻址访问,以提⾼效率
举个例⼦:
#include <iostream>
using namespace std;
class A
{
public:
inline A() {
cout << "inline construct()" <<endl;
}
inline ~A() {
cout << "inline destruct()" <<endl;
}
inline virtual void virtualFun() {
cout << "inline virtual function" <<endl;
}
};
int main()
{
A a;
a.virtualFun();
return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct()
构造函数和析构函数声明为内联函数是没有意义的
《Effective C++》中所阐述的是:将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不真正对声明为inline的构造和析构函数进⾏内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。其次,class中的函数默认是inline型的,编译器也只是有选择性的inline,将构造函数和析构函数声明为内联函数是没有什么意义的。
将虚函数声明为inline,要分情况讨论
有的⼈认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,⽽虚函数是运⾏期决定的,即在不知道将要调⽤哪个函数的情况下,如何将函数内联呢?
上述观点看似正确,其实不然,如果虚函数在编译器就能够决定将要调⽤哪个函数时,就能够内联,那么什么情况下编译器可以确定要调⽤哪个函数呢,答案是当⽤对象调⽤虚函数(此时不具有多态性)时,就内联展开
综上,当是指向派⽣类的指针(多态性)调⽤声明为inline的虚函数时,不会内联展开;当是对象本⾝调⽤虚函数时,会内联展开,当然前提依然是函数并不复杂的情况下auto、decltype和decltype(auto)的⽤法
auto
C++11新标准引⼊了auto类型说明符,⽤它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应某种特定的类型说明符(例如 int)不同,
auto 让编译器通过初始值来进⾏类型推演。从⽽获得定义变量的类型,所以说auto 定义的变量必须有初始值。举个例⼦:
//普通;类型
int a = 1, b = 3;
auto c = a + b;// c为int型
//const类型
const int i = 5;
auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int
auto k = &i; // 变量i是⼀个常量, 对常量取地址是⼀种底层const, 所以b的类型是const int*
const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前⾯加上cosnt
//引⽤和指针类型
int x = 2;
int& y = x;
auto z = y; //z是int型不是int& 型
auto& p1 = y; //p1是int&型
auto p2 = &x; //p2是指针类型int*
decltype
有的时候我们还会遇到这种情况,我们希望从表达式中推断出要定义变量的类型,但却不想⽤表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。在这些时候auto显得就⽆⼒了,所以C++11⼜引⼊了第⼆种类型说明符decltype,它的作⽤是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进⾏实际的计算表达式的值。
int func() {return 0};
/
/普通类型
decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调⽤函数func()
int a = 0;
decltype(a) b = 4; // a的类型是int, 所以b的类型也是int
//不论是顶层const还是底层const, decltype都会保留
const int c = 3;
decltype(c) d = c; // d的类型和c是⼀样的, 都是顶层const
int e = 4;
const int* f = &e; // f是底层const
decltype(f) g = f; // g也是底层const
//引⽤与指针类型
//1. 如果表达式是引⽤类型, 那么decltype的类型也是引⽤
const int i = 3, &j = i;
decltype(j) k = 5; // k的类型是 const int&
//2. 如果表达式是引⽤类型, 但是想要得到这个引⽤所指向的类型, 需要修改表达式:
int i = 3, &r = i;
decltype(r + 0) t = 5; // 此时是int类型
//3. 对指针的解引⽤操作返回的是引⽤类型
int i = 3, j = 6, *p = &i;
decltype(*p) c = j; // c是int&类型, c和j绑定在⼀起
//4. 如果⼀个表达式的类型不是引⽤, 但是我们需要推断出引⽤, 那么可以加上⼀对括号, 就变成了引⽤类型了
int i = 3;
decltype((i)) j = i; // 此时j的类型是int&类型, j和i绑定在了⼀起
decltype(auto)
decltype(auto)是C++14新增的类型指⽰符,可以⽤来声明变量以及指⽰函数返回类型。在使⽤时,会将“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例⼦:
int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e
volatile、mutable和explicit关键字的⽤法
volatile
volatile 关键字是⼀种类型修饰符,⽤它声明的类型变量表⽰可以被某些编译器未知的因素更改,⽐如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进⾏优化,从⽽可以提供对特殊地址的稳定访问。
当要求使⽤ volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前⾯的指令刚刚从该处读取过数据。
volatile定义变量的值是易变的,每次⽤到这个变量的值的时候都要去重新读取这个变量的值,⽽不是读寄存器内的备份。多线程中被⼏个任务共享的变量需要定义为volatile类型。volatile 指针
volatile 指针和 const 修饰词类似,const 有常量指针和指针常量的说法,volatile 也有相应的概念
修饰由指针指向的对象、数据是 const 或 volatile 的:
const char* cpch;
volatile char* vpch;
指针⾃⾝的值——⼀个代表地址的整数变量,是 const 或 volatile 的:
char* const pchc;
char* volatile pchv;
注意:
可以把⼀个⾮volatile int赋给volatile int,但是不能把⾮volatile对象赋给⼀个volatile对象。
除了基本类型外,对⽤户定义类型也可以⽤volatile类型进⾏修饰。
C++中⼀个有volatile标识符的类只能访问它接⼝的⼦集,⼀个由类的实现者控制的⼦集。⽤户只能⽤const_cast来获得对类型接⼝的完全访问。此外,volatile向const⼀样会从类传递到它的成员。
多线程下的volatile
有些变量是⽤volatile关键字声明的。当两个线程都要⽤到某⼀个变量且该变量的值会被改变时,应该⽤volatile声明,该关键字的作⽤是防⽌优化编译器把变量从内存装⼊CPU寄存器中。如果变量被装⼊寄存器,那么两个线程有可能⼀个使⽤内存中的变量,⼀个使⽤寄存器中的变量,这会造成程序的错误执⾏。volatile的意思是让编译器每次操作该变量时⼀定要从内存中真正取出,⽽不是使⽤已经存在寄存器中的值。
mutable
mutable的中⽂意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制⽽设置的。被mutable修饰的变量,将永远处于可变的状态,即使在⼀个const函数中。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数⼀般会声明
成const的。但是,有些时候,我们需要在const函数⾥⾯修改⼀些跟类状态⽆关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后⾯关键字位置。
explicit
explicit关键字⽤来修饰类的构造函数,被修饰的构造函数的类,不能发⽣相应的隐式类型转换,只能以显⽰的⽅式进⾏类型转换,注意以下⼏点:explicit 关键字只能⽤于类内部的构造函数声明上
explicit 关键字作⽤于单个参数的构造函数
被explicit修饰的构造函数的类,不能发⽣相应的隐式类型转换
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论