C++ Part3-进阶提高-QA

1. xxx.hpp的第一行中,#pragma once的含义是什么?

#pragma once 是预处理指令,用于确保头文件只被编译一次。这样可以避免由于多次包含同一头文件而导致的重复定义错误。通常情况下,#pragma once 会被放置在头文件的最开始位置。

这样写的目的是确保在编译时,头文件只被包含一次,以防止重复定义的问题。

上述这样说,还是有些抽象。如下这个反例可以帮助具体理解该#pragma once的作用。

当一个头文件被包含多次,且其中定义了全局变量或函数时,由于编译器会将头文件的内容复制到包含它的每个源文件中,就会导致全局变量或函数的重复定义错误。举例如下:

假设有一个头文件 example.h,其中定义了一个全局变量 int global_var 和一个函数 void foo()

 

// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H

int global_var = 10;

void foo() {
    std::cout << "Hello from foo!" << std::endl;
}

#endif

如果在多个源文件中都包含了该头文件,并且这些源文件都被编译,则会出现重复定义的错误,如下所示:

// main1.cpp
#include "example.h"

int main() {
    foo();
    return 0;
}

// main2.cpp
#include "example.h"

int main() {
    foo();
    return 0;
}

在这种情况下,global_varfoo() 都被定义了两次,编译器就会报重复定义的错误。而使用 #pragma once 可以确保头文件只被包含一次,避免了这种错误的发生。

 

2. 举例说明带const和不带const的区别,深入理解下细节。

举一个简单的例子,对比一下使用带 const 和不带 conststring 构造函数的区别。

#include <iostream>
#include <string>

int main() {
    char buffer[] = "Hello";
    std::string str1(buffer); // 使用非常量字符指针构造

    std::cout << "str1: " << str1 << std::endl; // 输出: str1: Hello

    buffer[0] = 'h'; // 修改了原始字符数组

    std::cout << "str1: " << str1 << std::endl; // 输出: str1: Hello (str1 的内容没有改变)
    std::cout << "buffer: " << buffer << std::endl; // 输出: buffer: hello

    return 0;
}

在这个例子中,我们使用了 string(char* s) 构造函数创建了 str1。虽然我们后续修改了 buffer 数组的内容,但是 str1 的内容并没有改变,因为在构造时,string 对象创建了自己的字符串副本。

const 的构造函数示例:

#include <iostream>
#include <string>

int main() {
    const char* literal = "Hello";
    std::string str2(literal); // 使用常量字符指针构造

    std::cout << "str2: " << str2 << std::endl; // 输出: str2: Hello

    // literal[0] = 'h'; // 编译错误,不能修改字面量

    return 0;
}

在这个例子中,我们使用了 string(const char* s) 构造函数创建了 str2。由于 literal 是一个字面量字符串,它是无法被修改的。如果我们尝试修改它,编译器会报错。

通过这两个例子,我们可以看到带 const 和不带 const 的区别:

  • 不带 const 的构造函数允许我们传入一个非常量字符指针,但是我们无法修改 string 对象内部的字符串内容
  • 带 const 的构造函数要求我们传入一个常量字符指针,并且我们无法修改该指针所指向的字符串内容

一般来说,我们应该尽量使用带 const 的构造函数,以避免意外修改字符串字面量。不过,如果你确实需要修改字符串内容,那么可以使用不带 const 的构造函数,并传入一个可修改的字符数组。

 

3. 为什么这行代码会报错, 请理解缘由。

char* str = "hello world";

这行代码会报错,因为它试图将一个字符串字面值(字面量)的地址赋给一个非常量字符指针。这是不允许的,因为字符串字面值存储在只读的内存段中,我们不能修改它们。

具体来说,以下是这行代码报错的原因和解释:

  1. 字符串字面值是常量 在 C++ 中,字符串字面值(如 "hello world")是常量字符串,它们在编译时就已经确定了值,并被存储在只读的内存段中。这意味着我们不能修改它们的内容。
  2. const char 可以指向字面值* 由于字符串字面值是常量,所以我们只能使用 const char* 类型的指针来指向它们。因为 const char* 指针指向的字符串是只读的,我们不会意外修改它们。
  3. char 不能指向字面值* 而在上述代码中,char* str = "hello world"; 试图将字符串字面值 "hello world" 的地址赋给一个非常量字符指针 char* str。这是不允许的,因为 char* 类型的指针可以修改它所指向的内容,而字符串字面值是存储在只读内存中的常量,不能被修改。

为了解决这个问题,我们有以下几种选择:

 (1).使用 const char 指针*
const char* str = "hello world";

(2).创建一个字符数组,并将字面值复制到该数组中

char str[] = "hello world";

这样就创建了一个可修改的字符数组,并将字符串字面值复制到了该数组中。我们可以修改这个数组的内容,但不能修改原始的字面值。

(3).使用 std::string 类

std::string str = "hello world";

使用 C++ 标准库中的 std::string 类来存储字符串。这种方式更加安全和方便,因为 std::string 对象会自动管理内存,并提供了许多有用的字符串操作函数。

总之,这个错误是由于 C++ 中字符串字面值的特性引起的。我们必须小心处理字符串字面值,不能试图去修改它们,除非先将它们复制到一个可修改的内存区域中。

 

4. 关于v1.capacity()的实际取值偏大的理解。

#include <iostream>
#include <vector>
using namespace std;

void printVector(vector<int>& v) {

    for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;
}


void test01()
{
    vector<int> v1;
    for (int i = 0; i < 10; i++) // 可以改变下 i < 的取值,发现capacity也是动态扩大的
    {
        v1.push_back(i);
    }

    printVector(v1);

    if (v1.empty())
    {
        cout << "v1为空" << endl;
    }else {
        cout << "v1不为空" << endl;
        cout << "v1的容量 = " << v1.capacity() << endl; // v1.capacity() = 13?
        cout << "v1的大小 = " << v1.size() << endl;
    }

    v1.resize(15, 10);
    printVector(v1);


    v1.resize(5);
    printVector(v1);

}


int main()
{
    test01();
    system("pause");
    return 0;
}

v1的容量(capacity())为13的原因是由于C++标准库中vector的内存管理策略造成的。

vector是C++标准库中的动态数组容器,它在内部使用一段连续的内存空间来存储元素。当vector的元素数量增加时,如果当前内存空间不足,它会自动分配一个更大的内存空间,并将原有数据复制到新的内存空间中。

在示例中,v1最初是一个空的vector。当执行v1.push_back(i)时,由于vector的初始容量通常是0或者一个较小的值,所以它需要先分配一段内存空间来存储新元素。

对于大多数C++标准库实现来说,vector在分配新的内存空间时,通常会分配比实际需求更大的内存空间,以便在将来添加更多元素时可以避免频繁地重新分配内存。这种策略被称为"增量分配"(Incremental Allocation)

具体来说,在您的代码中,当v1的元素数量达到某个阈值时(这个阈值因实现而异),vector会分配一个比实际需求更大的内存空间。在您的环境中,这个新分配的内存空间容量恰好是13。这就是为什么v1.capacity()返回13。

值得注意的是,vector的容量capacity()可能会因实现而异,甚至在不同的运行环境下也可能不同。但是无论如何,当vector的元素数量超过当前容量时,它都会自动分配一个更大的内存空间,以容纳更多的元素。

总之,v1容量为13是vector的内存管理策略造成的,这种策略可以提高vector在添加元素时的性能,但同时也可能导致占用更多的内存空间。

 

 5. 关于vector交换如何实现压缩内存的理解。

void test02()
{
    vector<int> v;
    
    for (int i = 0; i < 100000; i++)
    {
        v.push_back(i);
    }

    cout << "v的容量为: " << v.capacity() << endl; //138255
    cout << "v的大小为: " << v.size() << endl; //10000

    v.resize(3);
    cout << "v的容量为: " << v.capacity() << endl; //138255
    cout << "v的大小为: " << v.size() << endl; // 3

    //收缩内存
    //vector<int>(v) // 匿名对象,使用完后会被编译器回收
    vector<int>(v).swap(v);
    //cout << vector<int>(v).size() << endl; // 3 【也即vector<int>(按size算,而非capacity算) 】
    cout << "v的容量为: " << v.capacity() << endl; // 3
    cout << "v的大小为: " << v.size() << endl;  // 3 
}

 这段代码首先创建了一个空的 vector v,然后向其中添加了 100000 个整数,此时 v 的大小为 100000,但由于 vector 会动态调整内部容量以适应元素的添加,因此它的容量可能会大于 100000。

接着,代码打印出了 v 的容量和大小,并执行了 resize(3) 操作,将 v 的大小调整为 3。此时 v 的大小变为 3,但是容量仍可能保持不变,因为 resize() 函数只改变了大小而不影响容量

随后,代码通过创建一个匿名的临时 vector 对象,其中包含 v 的副本,然后使用 swap() 函数将临时对象的内存空间与原始的 v 进行了交换。在交换之后,原始的 v 就拥有了匿名对象的内存空间,而匿名对象则会在作用域结束后被销毁,因此原始 v 的容量会被压缩到仅容纳其当前大小的水平。最后,代码再次打印出了 v 的容量和大小。

这种技巧通过创建匿名对象来实现,而不需要手动释放内存,从而简化了代码,并且在代码执行后能够立即释放不再需要的内存,减少了内存占用。

 

 6. 为什么要在这个仿函数后面加上const才不会报错?请仔细理解

#include <iostream>
#include <set>
using namespace std;


class myCompare
{
public:
    bool operator()(int val1, int val2) const // const必须得加上的必要性,好好理解吃透
    {
        return val1 > val2;
    }
};


void test01()
{
    set<int> s1;
    s1.insert(10);
    s1.insert(40);
    s1.insert(20);
    s1.insert(30);
    s1.insert(50);

    for (set<int>::iterator it = s1.begin(); it != s1.end(); it++)
    {
        cout << *it << " ";
    }
    cout << endl;

    
    set<int, myCompare> s2;
    s2.insert(10);
    s2.insert(40);
    s2.insert(20);
    s2.insert(30);
    s2.insert(50);

    for (set<int, myCompare>::iterator it = s2.begin(); it != s2.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;

}


int main()
{
    test01();

    system("pause");
    return 0;
}

不加上const,  就会报错。这个错误是由于在 std::set 的底层实现中,它默认使用 less 这个函数对象作为比较器来维护集合的元素顺序。而在你的代码中,你定义了一个自定义的比较器 myCompare,它是一个类对象,而不是一个函数对象。

在 C++ 标准库中,函数对象是通过重载 operator() 来实现的。而类对象则不能直接作为函数对象使用,需要先将其包装成一个函数对象。

解决方案是将你的 myCompare 类声明为一个函数对象,即重载 operator()

 通俗的来解释,可以这样理解。

首先,我们需要理解一个概念:在 C++ 标准库中,像 std::set 这样的容器在内部是使用了一个函数对象(Function Object)来比较元素的大小,从而维护元素的有序性。这个函数对象必须是"常量"的,也就是说,它不能修改自身的状态或者修改传递给它的参数。

为什么要这样设计呢?这是因为容器在内部需要频繁地调用这个函数对象,如果函数对象不是常量的话,那么它可能会被意外地修改,从而导致容器的元素顺序被破坏,造成不可预知的后果。所以,为了保证容器的正确性和稳定性,标准库要求这个函数对象必须是"常量"的。

那么,如何让一个自定义的比较器函数对象成为"常量"呢?这就需要在 operator() 运算符重载函数后面加上 const 关键字了。

让我们来看一个例子:

struct MyCompare
{
    bool operator()(int a, int b) const // 注意这里的 const
    {
        return a > b; // 这个函数不会修改任何东西,它是一个"常量"函数
    }
};

在上面的代码中,我们定义了一个名为 MyCompare 的结构体,重载了 operator() 运算符,使它成为一个函数对象。关键是,我们在 operator() 后面加上了 const 关键字。

这个 const 关键字的作用是:告诉编译器,这个 operator() 函数是一个"常量成员函数",它不会修改当前对象的任何成员变量。因为它不会修改任何东西,所以我们可以把它看作是一个"常量"函数对象,满足了标准库容器的要求。

如果我们不加 const,编译器就会认为这个 operator() 函数有可能会修改当前对象的状态,从而不符合标准库容器对比较器函数对象的"常量"要求,所以会报错。

希望通过这个例子, 就可以更好地理解为什么我们需要在 operator() 后面加上 const 关键字了。总的来说,这是为了满足标准库容器对比较器函数对象的"常量"要求,从而保证容器的正确性和稳定性。

 

 7. 就6的问题进一步衍生。 我修改代码如下的(val1++), 实际并未发现编译器报错,这又是为什么呢?

class myCompare
{
public:
    bool operator()(int val1, int val2) const
    {    
        val1++; // 加了这行却没报错,这是为什么?
        return val1 > val2;
    }

private:
    int count = 0;
};

这段代码实际上是可以通过编译的,因为val1val2是作为函数参数传递进来的,它们是局部变量的拷贝,而不是类的成员变量

虽然operator()被声明为const成员函数,但这个const限定符只是约束了该函数不能修改当前对象的成员变量,而并不限制它修改函数参数或局部变量的值。

在这段代码中,val1++只是修改了val1这个局部变量的值而没有试图修改类的任何成员变量,因此编译器不会报错

但是,这种修改函数参数的做法在使用std::set时是不推荐的,因为它会导致比较器函数的行为不确定,从而影响容器的正确性。std::set期望比较器函数对象是"常量的",也就是说,它们不应该修改任何状态,包括局部变量和参数。

为了保证std::set的正确性,我们应该遵循编写"常量函数对象"的最佳实践,即:

  1. operator()函数中不修改任何参数的值。
  2. operator()函数中不修改任何局部变量的值(除非局部变量是constconstexpr类型)。
  3. operator()函数中不修改任何类成员变量的值。

通过遵循这些最佳实践,我们可以确保自定义的比较器函数对象是真正"常量的",从而满足std::set的要求,保证容器的正确性和稳定性。

总之,虽然您的示例代码可以通过编译,但它违反了编写"常量函数对象"的最佳实践,在使用std::set时可能会引入未定义的行为。我们应该避免修改函数参数和局部变量的值,以确保比较器函数对象的"常量性"。

 

 8. 解释下这一行代码的作用: srand((unsigned int)time(NULL));

这行代码的作用是设置随机数生成器的种子。让我们来分解它:

  1. time(NULL) 返回当前系统时间的秒数,以自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的秒数表示。它返回一个 time_t 类型的值,通常是一个整数。

  2. (unsigned int)time_t 类型的时间值强制转换为无符号整数,以确保生成的随机数种子是一个正数。

  3. srand() 是一个函数,用于设置随机数生成器的种子。种子确定了随机数生成器的初始状态。每次使用 rand() 函数生成随机数时,都会使用当前的种子来计算下一个随机数。如果种子相同,那么生成的随机数序列也将相同。

综合起来,这行代码的作用是利用当前时间来设置随机数生成器的种子,以确保每次程序运行时生成的随机数序列是不同的。这样做可以增加生成的随机数的随机性。

 

9. C++中重载运算符()的作用叫做仿函数,那请问编译器默认的()功能是什么?

在 C++ 中,编译器默认的 () 功能与对象的创建和函数调用相关联。具体来说,当我们使用类名称后跟一对括号 () 时,编译器会将其解释为对类的默认构造函数的调用。如果类没有显式定义构造函数,则编译器会提供一个默认的构造函数。

当我们创建对象时,可以使用括号 () 来调用构造函数,以初始化对象的成员变量。如果没有提供任何参数,则会调用默认构造函数。例如:

class MyClass {
public:
    // 默认构造函数
    MyClass() {
        std::cout << "Default constructor called." << std::endl;
    }
};

int main() {
    MyClass obj1; // 调用默认构造函数
    MyClass obj2(); // 这不是对象创建,而是声明了一个函数
    return 0;
}

在上面的例子中,MyClass obj1; 调用了默认构造函数,而 MyClass obj2(); 实际上声明了一个函数,而不是创建对象,因为括号中没有提供任何参数。

此外,括号 () 也用于调用函数。当我们调用函数时,函数名后跟一对括号,用于传递参数并执行函数体。例如:

void myFunction(int x) {
    std::cout << "Value of x: " << x << std::endl;
}

int main() {
    myFunction(10); // 调用函数 myFunction,并传递参数 10
    return 0;
}

在这个例子中,myFunction(10); 调用了名为 myFunction 的函数,并将参数 10 传递给它。

总之,编译器默认的 () 功能涉及到对象的创建和函数的调用,包括调用默认构造函数以及调用函数并传递参数。

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2024-10-14 16:56  AlphaGeek  阅读(13)  评论(0)    收藏  举报