关于最近见到的一部分CPP问题的思考与解答

前言

在逛知乎时,偶然遇到了这样一个回答:

这里有两组面试题,都是最基础的题目,但是轻松通过的话已经超过我面试过的所有人。
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这里就初见端倪:

  • 首先,由于startIndexshort类型,所以i也被推导为short类型,而short类型一般是2字节,对应的整数表示范围是-32768~32767,这里40000显然超出了其表示范围。换而言之,i将永远无法自增到40000,从而一直执行循环

  • 另外,如果编译器默认charsigned char,那么从shortsigned 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[]来删除,当然,对于一般的编译器而言,平凡析构的类型不严格地区分deletedelete[]是没什么太大问题的。但是对于对象数组而言,如果将delete[]换为delete,你很大概率会收获一个崩溃的程序。

具体原因可以参照:

总结下来就是,对于平凡析构的类型在析构时不必调用析构函数,因此只需要做内存回收即可,如同free(void*)做的那样。

而对非平凡析构的类型的数组,在析构时需要知道总共应该调用几次析构函数,这时就需要把数组内对象的个数存储下来,通常是在分配内存空间的头部,也就是会多分配一些内存。

这时,当我们尝试用delete去删除一个非平凡析构类型的数组时,实际上是从第一个对象的地址开始析构而不是从已分配位置的头部,因此就会造成段错误。

关于析构函数的设置

即使我们将delete[]修改正确,这个程序仍然存在问题:

  1. 用同一个指针pVar来初始化类中的m_pVar1,而m_pVar1又在析构函数中被析构,这样调用delete[]依次析构这10个对象时就会析构10次该指针,是未定义行为,大概率会引起段错误。
  2. 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进行修改。

解决方法是:

  • 去除函数AddAndCheckconst修饰符
  • 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 };
}

首先,pBufferbuffer的不同就是一个是在堆上分配,而一个是在栈上分配,而我们要分配的大小是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就指向var1var是到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()

可见:Microsoft Doc

在Linux系统下,可以利用系/include/linux/init.h头文件中的__init宏来标记函数,从而使其在主函数前执行。

Part B. Question 10

What the difference are between
std::vector::resize() and std::vector::reserve()
std::vector::size() and std::vector::capacity()

size & capacity

在谈及resize & reserve的差别之前,我们需要先了解sizecapacity的区别:

  • size即容器的大小,代表了这个容器内具体有多少个元素
  • capacity是容器的容量,即这个容器在当前分配的内存下最多能够容纳多少个元素

如果size()==capacity(),那么就代表这个容器内的元素已经填满了当前分配的内存。

resize & reserve

resizereserve分别紧密联系着sizecapacity,对于一个有剩余空间的容器而言(即size < capacity),resize只会改变size的大小,而reserve只会改变capacity的大小。

除去resizereservesizecapacity的影响外,它们在对内存的行为上也有所不同:

  • resize在容器的容量充足时,不会申请新的内存,只会对当前的元素进行追加或者丢弃
  • resize在容器的容量不足时,才会申请新的内存,同时改变capacity的值,不过,申请的新内存的长度是不定的
  • reserve是扩充容器的容量,当参数小于等于容器容量时,这个函数不做任何事情;当参数大于容易容量时,就会重新申请内存,内存的大小取决于传入的容量大小。并且,调用reserve后,push_back等插入操作不再重新分配内存,除非插入导致size > capacity
  • 而且无论是resize还是reserve,一旦涉及到容量的扩容,那么就会引起迭代器失效。

剩下的题以后再更新……

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
隐藏
变装