前言
在逛知乎时,偶然遇到了这样一个回答:
这里有两组面试题,都是最基础的题目,但是轻松通过的话已经超过我面试过的所有人。
Welcome to the C/C++ Quiz!C/C++ Quiz 2
本文就该页面提到的题目进行思考与解答(注:默认少打的分号、拼错的变量、函数、类等名字不算错误并已更正)
C++ Quiz 2
Part B. Question 1
What are wrong in the following code?
void foo(short startIndex)
{
short buffer[50000] = { 0 };
for (auto i = startIndex; i < 40000; ++i)
{
buffer[i] = (char)i;
}
}
首先注意到,题目中的整形的数据类型是short
,问题在auto i = startIndex
这里就初见端倪:
-
首先,由于
startIndex
是short
类型,所以i
也被推导为short
类型,而short
类型一般是2字节,对应的整数表示范围是-32768~32767,这里40000显然超出了其表示范围。换而言之,i
将永远无法自增到40000,从而一直执行循环。 -
另外,如果编译器默认
char
是signed char
,那么从short
到signed char
的窄化转换是由实现定义的的,你不能确定它的具体行为,这里应换为unsigned char
。
上面即是这个程序存在的问题
注:从C++20开始,这一窄化转换是被定义的,结果是用源类型的值与2N取模,N是目标类型的长度。
Part B. Question 2
Mark all lines which are wrong and provide the FIX.
class CBase
{
public:
CBase()
: m_pVar1(NULL)
{
m_pVar2 = new int(4);
}
~CBase()
{
delete m_pVar2;
delete m_pVar1;
}
void Init(int* pVar)
{
m_pVar1 = pVar;
}
private:
int* m_pVar1;
int* m_pVar2;
};
class CDerive : public CBase
{
public:
CDerive(int var) : m_var(var) {};
~CDerive() {};
private:
int m_var;
};
int main()
{
CDerive* pDerives = new CDerive[10];
int *pVar = new int(10);
for (int i = 0; i < 10; ++i)
{
pDerives[i].Init(pVar);
}
delete pDerives;
delete pVar;
}
先从比较容易看出来的地方入手:
观察主函数的第一行,这里初始化了一个对象数组,但是CDerive
类只有唯一构造器ctor(int)
,这里显然不满足条件
-
解决方法一:应该具体构造每一个数组元素:
CDerive* pDerives = new CDerive[10] { CDerive(1), CDerive(2), ... };
当构造函数隐式时,可以略去类名:
CDerive* pDerives = new CDerive[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
-
解决方法二:如果我们不打算具体构造每一个数组元素,那么应该提供无参的构造函数以供
new
来调用。 -
解决方法三:或者,我们不适用
new <typename>
的形式来分配内存,而是:pDerives = static_cast<CDerive*>(::operator new(sizeof(CDerive) * 10)); // 这里使用allocator也可
然后调用对象的某些函数来对类成员进行设置。
我们不考虑allocator或::operator new/new[]
(如果使用,那么调用destruct()
或者::opeartor delete/delete[]
即可)接下来:
既然我们使用new
初始化了一个数组,应该使用delete[]
来删除,当然,对于一般的编译器而言,平凡析构的类型不严格地区分delete
与delete[]
是没什么太大问题的。但是对于对象数组而言,如果将delete[]
换为delete
,你很大概率会收获一个崩溃的程序。
具体原因可以参照:
总结下来就是,对于平凡析构的类型在析构时不必调用析构函数,因此只需要做内存回收即可,如同free(void*)
做的那样。
而对非平凡析构的类型的数组,在析构时需要知道总共应该调用几次析构函数,这时就需要把数组内对象的个数存储下来,通常是在分配内存空间的头部,也就是会多分配一些内存。
这时,当我们尝试用delete
去删除一个非平凡析构类型的数组时,实际上是从第一个对象的地址开始析构而不是从已分配位置的头部,因此就会造成段错误。
关于析构函数的设置
即使我们将delete[]
修改正确,这个程序仍然存在问题:
- 用同一个指针
pVar
来初始化类中的m_pVar1
,而m_pVar1
又在析构函数中被析构,这样调用delete[]
依次析构这10个对象时就会析构10次该指针,是未定义行为,大概率会引起段错误。 CDerive
对象析构完毕后又再次析构了pVar
,也是对同一块内存的重复回收,同样是未定义行为。
解决方法是:在析构函数中不删除m_pVar1
而是将其置空,因为m_pVar1
依赖于外部传入的指针,你无法预测外部对该指针的行为,因此析构时选择将其置为nullptr
更好,这样修改的同时delete pVar
也能成功。
Part B. Question 3
The following code could not be compiled, give TWO ways to fix it.
class CFoo
{
public:
CFoo() : m_var(0) {};
~CFoo() {};
bool AddAndCheck() const
{
m_var += 5;
return m_var < 10;
}
private:
int m_var;
}
这个问题属于比较简单的一个,其原因在于const
函数内部*this
也是const
的,因此*this.m_var
也是const int
类型,不能对const int
进行修改。
解决方法是:
- 去除函数
AddAndCheck
的const
修饰符 - 将
m_var
声明成mutable int
类型
Part B. Question 4
What are wrong in the following code? Provide your fix.
#define INCREASE(a) ++a
void foo()
{
int a = 3;
int b = 7;
cout<<INCREASE(a * b)<<endl;
}
宏本质上就是代码的展开和替换,因此,实际上这里INCREASE(a * b)
展开后就是++a * b
,那么上述代码有两个问题:
- 语义不符,上述代码的实际结果是输出(a+1)*b的值,并且会让a自增一次
- 即使将
INCREASE(a)
改为INCREASE(a) (++(a))
也存在问题,因为不一定能将++
运算符应用到a*b
这个表达式的返回值上。
从语义上来讲,INCREASE(a)
应该期待在原地返回表达式a加一的值,并且对表达式a
进行一次自增
要达成前面的语义,可以改为:
#define INCREASE(a) ((a) + 1)
要达成后面的语义,则避免向INCREASE(a)
传入表达式:
#define INCREASE(a) (++(a))
int c = a * b;
cout << INCREASE(c) << endl;
或者,表达式(a)
应该至少返回一个能够应用自增运算符的类型:
// INCREASE(c)
A a, b;
cout << (++(a * b)) << endl; // A::opeartor++
Part B. Question 5
What are wrong in the following code? Why?
char* GetStaticBuffer()
{
char buffer[100] = { 0 };
return buffer;
}
典型的返回指向栈上变量的指针,buffer
的内存在GetStaticBuffer()
调用完毕时就会被回收,此时返回去的指针就是一个无法使用的野指针。
IDE也会这样提示:The address of the local variable ‘buffer’ may escape the function
从题目的函数名考虑,这里的语义显然是期待返回一个静态的buffer,那么可以:
char* getStaticBuffer()
{
static char buffer[100] = { 0 };
return buffer;
}
Part B. Question 6
With default configuration, in the following code, function fooA is OK but fooB has issues, why?
const int TEN_MEGA_BYTES = 10 * 1024 * 1024;
void fooA()
{
char *pBuffer = new char[TEN_MEGA_BYTES];
}
void fooB()
{
char buffer[TEN_MEGA_BYTES] = { 0 };
}
首先,pBuffer
与buffer
的不同就是一个是在堆上分配,而一个是在栈上分配,而我们要分配的大小是TEN_MEGA_BYTES * sizeof(char)
,按照该程序即10MB,而栈空间的大小在Linux系统下默认是8MB,调用fooB()
会导致栈溢出。
Part B. Question 7
In the following code, line 32, I want to give "student_teacher" as the input parameter, but by mistake, I typed "student". The compiling succeeded anyway. Why? If I want the compiling be failed in this condition, how to do?
class CStudent
{
public:
CStudent() {};
~CStudent() {};
};
class CTeacher
{
public:
// 原题这里也是CTeacher(CStudent student)
// 而下面又默认初始化了,这里更正过来
CTeacher() {};
CTeacher(CStudent student)
: m_student(student)
{
}
~CTeacher() {};
void Teach() {};
private:
CStudent m_student;
};
void foo(CTeacher teacher)
{
teacher.Teach();
}
int main()
{
CStudent student;
CTeacher student_teacher;
foo(student);
}
首先来看,foo()
函数接受一个CTeacher
类型的参数,而我们在调用的时候传入的是CStudent
类型的参数,编译却没有失败,原因是:
CTeacher
类型有一个接受CStudent
类型对象的构造函数,且非explicit
,因此这里实际上是隐式地调用这一个构造函数,利用student
对象创建了一个CTeacher
类型的对象,再传入foo()
,因此不会编译失败。
要让这一程序产生编译错误,可以:
- 将
CTeacher(CStudent)
构造函数声明为explicit
- 将
foo
的参数类型从CTeacher
改为CTeacher&
,注意,如果不更改CTeacher(CStudent)
为explicit
,这里也不能改为CTeacher&&
,否则编译一样通过 - 其实,更改为
const CTeacher&
一样可行,只是出错的点不同,这里是const CTeacher
类型的对象无法调用非const
修饰的成员函数,只是似乎不太符合题目中想要达到的语义。
Part B. Question 8
What is the output of the following code?
int fooVar = 10;
void foo(int *pVar, int& var)
{
pVar = &fooVar;
var = fooVar;
}
int main()
{
int var1 = 1;
int var2 = 2;
foo(&var1, var2);
cout<<var1<<":"<<var2<<endl;
}
按照上面的方式调用foo
,那么pVar
就指向var1
,var
是到var2
的引用。
因此,var=fooVar
实际上是修改了var2
的值,即var2=10
而修改pVar
实际上是修改了foo
栈上的pVar
中的地址信息,让pVar
指向fooVar
,对var1
无影响
所以这个程序输出为:
1:10
Part B. Question 9
How to run some code before main function is called?
在主函数调用前调用其他函数,可以有以下方法:
1. 利用编译器扩展
可以使用GCC/Clang编译器的扩展__attribute__((constructor))
来标记一个函数,或者写为[[gnu::constructor]]
2. 利用全局变量的特性
利用类型的构造函数:
struct A
{
A()
{ /* run som code here */ }
} var;
int main()
{
return 0;
}
通过函数调用为全局变量初始化:
int foo()
{
/* run some code here */
return 0;
}
int var = foo();
或者将函数调用换为lambda表达式的调用:
int var = []() -> int {
/* run some code */
return 0;
}();
3. 利用不同系统调用main函数前的行为
在Windows环境下,调用主函数前会先调用CRTInit
,而CRTInit
函数又会依据一张表来调用一些函数,我们只需要把想要执行的函数注册在这张表内即可:
#pragma data_seg(".CRT$XCU")
static <func-type>* <var-name> = { <func-name> };
#pragma data_seg()
在Linux系统下,可以利用系/include/linux/init.h
头文件中的__init
宏来标记函数,从而使其在主函数前执行。
Part B. Question 10
What the difference are between
std::vector::resize()
andstd::vector::reserve()
std::vector::size()
andstd::vector::capacity()
size & capacity
在谈及resize
& reserve
的差别之前,我们需要先了解size
与capacity
的区别:
size
即容器的大小,代表了这个容器内具体有多少个元素capacity
是容器的容量,即这个容器在当前分配的内存下最多能够容纳多少个元素
如果size()==capacity()
,那么就代表这个容器内的元素已经填满了当前分配的内存。
resize & reserve
resize
和reserve
分别紧密联系着size
和capacity
,对于一个有剩余空间的容器而言(即size < capacity
),resize
只会改变size
的大小,而reserve
只会改变capacity
的大小。
除去resize
和reserve
对size
和capacity
的影响外,它们在对内存的行为上也有所不同:
resize
在容器的容量充足时,不会申请新的内存,只会对当前的元素进行追加或者丢弃resize
在容器的容量不足时,才会申请新的内存,同时改变capacity
的值,不过,申请的新内存的长度是不定的。reserve
是扩充容器的容量,当参数小于等于容器容量时,这个函数不做任何事情;当参数大于容易容量时,就会重新申请内存,内存的大小取决于传入的容量大小。并且,调用reserve
后,push_back
等插入操作不再重新分配内存,除非插入导致size > capacity
- 而且无论是
resize
还是reserve
,一旦涉及到容量的扩容,那么就会引起迭代器失效。
剩下的题以后再更新……