Bootstrap

C++学习笔记05-偏八股向知识(问题-解答自查版)

前言

以下问题以Q&A形式记录,基本上都是笔者在初学一轮后,掌握不牢或者频繁忘记的点

Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系,也适合做查漏补缺和复盘。

本文对读者可以用作自查,答案在后面,需要时自行对照。


问题集

Q1:INT_MAX这个宏定义的具体数值是由什么决定的?

Q2:以下写法哪个有问题?

        1)const int* a = &temp;

        2)int const *a = &temp;

        3)int* const p = &temp;

Q3:static关键字的作用

        Q3.1:函数内static变量

        Q3.2:类内static函数?如何理解:静态函数属于类而不是类的实例?

        Q3.3:静态成员变量和静态函数,哪个只能在类外定义?

Q4:

        1)new和malloc的区别?(返回&空间&语法元素&用法)

        2)什么是自由存储区?

Q5:constexpr?怎么用?

Q6:volatile?

Q7:对于运算符"++"的重载,关于前置++与后置++的问题:

        1)为什么后置++的函数需要返回对象,而不是引用?

        2)为什么后置前面也要加const?

Q8:a++ 和 int a = b 在C++中是否是线程安全的?如何解决?

Q9:int (*ptr)(char); 和 int *ptr(char); 的区别?

Q10:什么是回调机制?如何实现?

Q11:关于cast强制转换的问题

Q11.1:解释:

        double b = static_cast<double>(a);

        Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

        int* b = const_cast<int*>(a);

Q11.2:这段代码中,p的动态转换为什么会抛出异常?

class Base {
public:
    int no;
    virtual void fun(){}
};

class Person : public Base{
public:
    int age;
};

int main(){
    Base b;
    Person p = dynamic_cast<Person &>(b);        // 这里为什么抛出异常?

Q13:动态强制转换 dynamic_cast是如何知道“enemy是一个Enemy对象”?C++的多态和C#的实现有何不同?

Q14:内存四区?

Q15:没有将基类的析构函数定义为虚函数,可能会导致什么问题?

Q16:如何防止内存泄露?

Q17:构造函数设为虚函数是有什么说法吗?

Q18: std::weak_ptr (弱引用智能指针)?

Q19:delete 释放的内存块的指针值会被设置为?

Q20:如何避免指针

Q21:野指针和悬浮指针的区别

Q22:如何避免悬浮指针

Q23:析运行下面的Test函数会有什么样的结果。

void GetMemory1(char* p)

{

 p = (char*)malloc(100);

}

void Test1(void)

{

 char* str = NULL;

 GetMemory1(str);

 strcpy(str, "hello world");

 printf(str);

}

Q24:注意看这个free的位置,这段代码有什么问题?

void Test4(void)

{

 char *str = (char*)malloc(100); strcpy(str, "hello");

 free(str);

 if(str != NULL) {

 strcpy(str, "world");

 cout << str << endl;

 }

}


参考解答

Q1:INT_MAX这个宏定义的具体数值是由什么决定的?

A1:可以。头文件 <climits> 定义了符号常亮:例如:INT_MAX 表示 int 的最大值,INT_MIN 表示 int 的最小值。

INT_MAX 的值是由编译器和平台的整数大小决定的,而不是由 C++ 标准直接规定。

C++ 标准只规定了 int 类型必须至少有 16 位,但是大多数现代编译器都使用 32 位的 int 类型。

如果想知道你的系统上 INT_MAX 的具体值,可以简单地包含头文件并打印出来:

#include <iostream>
#include <climits>

int main() {
    std::cout << "INT_MAX is " << INT_MAX << std::endl;
    return 0;
}

Q2:以下写法哪个有问题?顶层指针和底层指针?

1)const int* a = &temp;

2)int const *a = &temp;

3)int* const p = &temp;

A2:哪个都没问题。对于1和2的合法性容易有记忆上的漏洞。

        1和2都是底层const,指向的值不变。3是顶层指针,本身指向不变。

        术语 "顶层" 和 "底层" 来源于const 关键字在声明中的相对位置。"顶层" 指的是指针变量本身的层面,而 "底层" 指的是指针所指向的数据的层面。这种命名方式有助于清晰地区分不同层面上的 const 限定,从而更好地理解指针的行为和限制。

        本质上const类似于修饰,看修饰的是 *a 还是 p

Q3:static关键字的作用

Q3.1:函数内static变量

void exampleFunction() {

 static int count = 0; //

 count++;

 cout << "Count: " << count << endl;

}

A3.1:静态变量:在函数内部使用 static 关键字修饰。

在程序的整个生命周期内存在,不会因为离开作用域而被销毁。

Q3.2:类内static函数?如何理解:静态函数属于类而不是类的实例?

class ExampleClass {

public:

 static void staticFunction() {

 cout << "Static function" << endl;

 }

};

A3.2:和C#一样,静态函数属于类而不是类的实例,可以通过类名直接调用,而无需创建对象。

特点:不能调用非静态成员变量或者非静态函数。因为不能让非静态内容在它们生命周期开始前获知。

Q3.3:静态成员变量和静态函数,哪个只能在类外定义?

A:静态成员变量必须在类外部单独定义,以便为其分配存储空间。这是因为静态成员变量不属于类的任何特定实例,而是该类的所有实例共享的单一变量。由于静态成员变量与类的任何特定对象无关,它们在程序的全局命名空间中存在,因此需要在类外进行初始化。

class ExampleClass {

public:

   static int staticVar; // 静态成员变量声明,合法

};

// 静态成员变量定义

int ExampleClass::staticVar = 0;

如果想在类内初始化,唯一的办法是加上const:

class ExampleClass {

public:

        const static int staticVar; // 静态成员变量声明,合法

};

Q4:

1)new和malloc的区别?(返回&空间&语法元素&用法)

2)什么是自由存储区?

A4:

注意两个点:

1)new封装了malloc,只是添加了更多的内存维护

2)new在自由存储区上,实际上自由存储区也是堆的一部分,专门用来支持构造和析构的自动调用。

Q5:constexpr?怎么用?

A5:

const 表示“只读”的语义,constexpr 表示“常量”的语义

复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。

必须使用常量初始化:

Q6:volatile?

A6:与该变量有关的运算,不要进行编译优化;    // 不许抄近路

会从内存中重新装载内容,而不是直接从寄存器拷贝内容。    // 运行时变慢

使用场合:

在中断服务程序和CPU相关寄存器的定义

Q7:对于运算符"++"的重载,关于前置++与后置++的问题:

1)为什么后置++的函数需要返回对象,而不是引用?

2)为什么后置前面也要加const?

A7:

Q8:a++ 和 int a = b 在C++中是否是线程安全的?如何解决?

A8:都不是,编译器视角下都不是原子的。

C++11新标准提供了对整形变量原⼦操作的相关库,即std::atomic

        std::atomic 是个模板类,具体用法:

                std::atomic<int> value;

                value = 99;

        这里有个坑:

        std::atomic<int> value = 99; 这个语法不能在g++上通过,因为禁止拷贝构造自动生成

Q9:int (*ptr)(char); 和 int *ptr(char); 的区别?

A9:

1)int (*ptr)(char); 是在声明一个指向函数的指针。这个指针可以指向一个接受 char 类型参数并返回 int 类型的函数。

2)而 int *ptr(char); 则不是声明一个函数指针,而是声明了一个函数。其修正格式应该是:int* ptr(char)

这个函数接受一个 char 类型的参数,并返回一个指向 int 的指针。

关于函数指针的示例代码:

int main(){

    int (*ptr)(char);
    ptr = &show;
    cout << ptr('y') << (*ptr)('y') << endl;   
     
    // ptr('y')  
    // (*ptr)('y')
    // 这两种调用方式在功能上是等价的,都能达到调用函数指针指向的函数的目的。然而,使用 
    // ptr('y') 的方式更为简洁,通常在C++中更常见。

Q10:什么是回调机制?如何实现?

A10:回调机制是一种编程模式,允许将一个函数作为参数传递给另一个函数,然后在需要的时候从内部灵活调用。

所以用上面讲的函数指针方法就可以实现。我们使用的 sort 算法就属于回调的一种应用。

除此之外,界面的用户操作处理也会用到

Q11.1:解释以下cast强制转换变量的作用:

        double b = static_cast<double>(a);

        Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);

        int* b = const_cast<int*>(a);

A11:

强制转换

C风格的方法: int a = (int)c

C++风格的方法:

1)静态转换:用于非多态类型的转换,如基本数据类型,编译时而非运行时进行检查

int a = 10;

double b = static_cast<double>(a); // 将int转换为double

2)动态转换:用于处理多态性,只能在含有虚函数的类层次结构中使用

class Base { virtual void dummy() {} };

class Derived : public Base {};

Base* basePtr = new Derived();

Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);// 父转子

if (derivedPtr) {

// 转换成功

}

3)const_cast:用来越界修改const值,emmmm...

使得不能改写的const类型数据变得可以修改:

const int* a = new int(10);

int* b = const_cast<int*>(a); // 移除const限定符

Q11.2:这段代码中,p的动态转换为什么会抛出异常?

class Base {
public:
    int no;
    virtual void fun(){}
};

class Person : public Base{
public:
    int age;
};

int main(){
    Base b
    Person p = dynamic_cast<Person &>(b);        // 这里为什么抛出异常?

A11.2:

类型不匹配:b 是 Base 类型的对象,而 Person 是从 Base 派生的类。dynamic_cast 用于类层次结构中的向下转型,即从基类向派生类转换。然而,这里的 b 实际上并不是 Person 类型的对象,而仅仅是 Base 类型的对象。

dynamic_cast 要求:dynamic_cast 需要对象实际上是目标类型的实例或者派生自目标类型。由于 b 不是 Person 或其派生类的实例,dynamic_cast 无法安全地进行转换

Q12:对于动态强制转换,如何应用?

A12:安全的向下转型:主要用来“将Entity对象转化为Player或者Enemy对象”

我们假设基类Entity有两个子类:Player和Enemy,

通常情况下,Player或者Enemy转化成Entity比较简单,因为他们本来就是,只需要隐式转换即可、

但是“Entity转化为Player或者Enemy”,就必须要cast方法进行强制转化。

工程中,使用的指针可能是个什么样的对象未必清楚,因此这个功能主要用来做验证,

比如尝试进行转换 player→entity 是合法的,而 player→enemy 就会返回nullptr;

如果不这样做而是用 强制转换的方法(如 :player = (Player *)enemy ),语法上能通过,但是访问虚空成员就容易产生崩溃。

所以,工程上一定是用dynamic_cast的安全方法来进行对象的转换和验证

int main() {

    Animal* aa = new Bird; // 注意这里创建的是Bird对象,但类型是Animal*

    aa->func();             // 多态性:调用Bird的func

    Bird* bb = dynamic_cast<Bird*>(aa); // 动态转换

    if (bb) {

        bb->func(); // 调用Bird的func

    } else {

        std::cout << "dynamic_cast failed" << std::endl;

    }

    delete aa;

    return 0;

}

Q13:动态强制转换dynamic_cast是如何知道“enemy是一个Enemy对象”?C++的多态和C#的实现有何不同?

A13:dynamic_cast主要是通过RTTI(运行时类型识别)进行识别的,这种方式会使程序产生一定的开销。

在C++中,多态性主要通过虚函数和基类指针或引用来实现,而装箱和拆箱通常与C#等语言中的值类型和引用类型有关。

C++多态性是通过虚函数和运行时类型识别(RTTI)实现的,而C#装箱和拆箱是通过语言的类型系统和垃圾回收机制实现的。

Q14:内存四区?

A14:堆栈全常,还有个代码区

Q15:没有将基类的析构函数定义为虚函数,可能会导致什么问题?

A15:内存泄漏!

基类指针 p_entity 指向子类对象 player时,如果 Entity类的析构不是virtual的

那么这种情况下释放 p_entity 时不会调用子类析构函数,子类没正常释放资源,就会产生内存泄漏

Q16:如何防止内存泄露?

A16将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使用智能指针

Q17:构造函数设为虚函数是有什么说法吗?

A17:没有意义。构造函数不应该被定义为虚函数

Q18: std::weak_ptr (弱引用智能指针)?

A18:std::weak_ptr 可以从 std::shared_ptr 创建,但不会增加引用计数,不会影响资源的释放。

通过 std::weak_ptr::lock() 可以获取⼀个 std::shared_ptr 来访问资源。

#include <memory>

std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);

std::weak_ptr<int> weakPtr = sharedPtr;

Q19:delete 释放的内存块的指针值会被设置为?

A19:delete 释放的内存块的指针值会被设置为 nullptr ,以避免野指针。

free 不会修改指针的值,可能导致野指针问题。

Q20:如何避免指针

A20:

1)在释放内存后将指针置为 nullptr

int* ptr = new int;

// 使⽤ ptr 操作内存

delete ptr;

ptr = nullptr; // 避免成为野指针

2)使用函数返回指针变量时,避免返回局部变量的指针;

3)使用智能指针:避免显式 delete,指针会在超出作⽤域时⾃动释放

Q21:野指针和悬浮指针的区别

A21:野指针一般涉及指针问题。危害:可能导致访问已释放或无效内存,引发崩溃或数据损坏。

悬浮指针一般是引用问题,即指向已被delete或free销毁对象的引用。危害:可能导致访问销毁对象的未定义行为

Q22:如何避免悬浮指针

A22:悬浮指针指向的内存可能已经被释放或重新分配,导致未定义行为和程序错误。其避免方法有:

使用智能指针:如 std::unique_ptr、std::shared_ptr 和 std::weak_ptr,自动管理内存,减少手动管理内存的需要。

 std::unique_ptr<int> ptr(new int); // 使用unique_ptr管理内存

避免跨函数返回局部指针:不要从函数返回局部变量的指针,因为局部变量在函数返回后生命周期结束。

int* getPointer() {

    int localValue = 42;

    return &localValue; // 错误:返回了局部变量的地址

}

Q23:析运行下面的Test函数会有什么样的结果。

void GetMemory1(char* p)

{

         p = (char*)malloc(100);

}

void Test1(void)

{

         char* str = NULL;

         GetMemory1(str);

         strcpy(str, "hello world");

         printf(str);

}

A23:运行 Test1 函数的结果很可能是程序崩溃,因为 strcpy 试图向一个空指针写入数据。

为什么说GetMemory1并不能传递动态内存?

void GetMemory1(char* p)

{

         p = (char*)malloc(100);

}

在C语言中,函数参数传递是通过值传递来实现的,也就是说,当一个变量作为参数传递给函数时,实际上是传递了这个变量的一个副本。在你提供的GetMemory1函数中,参数p是一个char*类型的指针,它指向一个字符。

当你在函数内部使用malloc来分配内存并尝试将p指向这块新分配的内存时:

p = (char*)malloc(100);

这里,p指针的副本被修改了,它现在指向了新分配的内存区域。但是,这个修改只发生在函数内部,原始的指针(即传递给函数的那个副本)并没有被改变。因此,当函数执行完毕后,返回到调用函数时,原始的指针仍然指向它原来的位置,而不是新分配的内存区域。

这就是为什么说GetMemory1不能传递动态内存的原因。要正确地传递动态内存,你需要使用指针的指针或者引用的引用,这样修改内部的指针就可以反映到外部的指针上。例如:

void GetMemory2(char** p)

{

    *p = (char*)malloc(100);

}

Q24:注意看这个free的位置,这段代码有什么问题?

void Test4(void)

{

 char *str = (char*)malloc(100); strcpy(str, "hello");

 free(str);

 if(str != NULL) {

 strcpy(str, "world");

 cout << str << endl;

 }

}

A24:篡改动态内存区的内容,后果难以预料。非常危险。

因为 free(str);之后,str成为野指针,if(str != NULL)语句不起作用。

;