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_var 和 foo() 都被定义了两次,编译器就会报重复定义的错误。而使用 #pragma once 可以确保头文件只被包含一次,避免了这种错误的发生。
2. 举例说明带const和不带const的区别,深入理解下细节。
举一个简单的例子,对比一下使用带 const 和不带 const 的 string 构造函数的区别。
#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";
这行代码会报错,因为它试图将一个字符串字面值(字面量)的地址赋给一个非常量字符指针。这是不允许的,因为字符串字面值存储在只读的内存段中,我们不能修改它们。
具体来说,以下是这行代码报错的原因和解释:
- 字符串字面值是常量 在 C++ 中,字符串字面值(如 "hello world")是常量字符串,它们在编译时就已经确定了值,并被存储在只读的内存段中。这意味着我们不能修改它们的内容。
- const char 可以指向字面值* 由于字符串字面值是常量,所以我们只能使用
const char*类型的指针来指向它们。因为const char*指针指向的字符串是只读的,我们不会意外修改它们。 - char 不能指向字面值* 而在上述代码中,
char* str = "hello world";试图将字符串字面值 "hello world" 的地址赋给一个非常量字符指针char* str。这是不允许的,因为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; };
这段代码实际上是可以通过编译的,因为val1和val2是作为函数参数传递进来的,它们是局部变量的拷贝,而不是类的成员变量。
虽然operator()被声明为const成员函数,但这个const限定符只是约束了该函数不能修改当前对象的成员变量,而并不限制它修改函数参数或局部变量的值。
在这段代码中,val1++只是修改了val1这个局部变量的值, 而没有试图修改类的任何成员变量,因此编译器不会报错。
但是,这种修改函数参数的做法在使用std::set时是不推荐的,因为它会导致比较器函数的行为不确定,从而影响容器的正确性。std::set期望比较器函数对象是"常量的",也就是说,它们不应该修改任何状态,包括局部变量和参数。
为了保证std::set的正确性,我们应该遵循编写"常量函数对象"的最佳实践,即:
- 在
operator()函数中不修改任何参数的值。 - 在
operator()函数中不修改任何局部变量的值(除非局部变量是const或constexpr类型)。 - 在
operator()函数中不修改任何类成员变量的值。
通过遵循这些最佳实践,我们可以确保自定义的比较器函数对象是真正"常量的",从而满足std::set的要求,保证容器的正确性和稳定性。
总之,虽然您的示例代码可以通过编译,但它违反了编写"常量函数对象"的最佳实践,在使用std::set时可能会引入未定义的行为。我们应该避免修改函数参数和局部变量的值,以确保比较器函数对象的"常量性"。
8. 解释下这一行代码的作用: srand((unsigned int)time(NULL));
这行代码的作用是设置随机数生成器的种子。让我们来分解它:
-
time(NULL)返回当前系统时间的秒数,以自 1970 年 1 月 1 日 00:00:00 UTC 以来经过的秒数表示。它返回一个time_t类型的值,通常是一个整数。 -
(unsigned int)将time_t类型的时间值强制转换为无符号整数,以确保生成的随机数种子是一个正数。 -
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 传递给它。
总之,编译器默认的 () 功能涉及到对象的创建和函数的调用,包括调用默认构造函数以及调用函数并传递参数。

浙公网安备 33010602011771号