C++ 参数传递 笔记

目录

1、输入参数的传递方式-选择传值还是传引用?

处理用户信息

处理坐标

处理配置

处理ID

2、对于需要修改的参数,使用非const引用传递

        有趣的例外:警惕表象的迷惑

需要警惕的陷阱 

“糟糕”的update方法:

“完美”的set_name与set_age方法:

总结与最佳实践:

如何避免掉进这些坑?

利用编译器的警告和错误:

遵循const正确性:

明确函数意图:

使用std::optional或返回值:

代码审查和测试:

了解编译器的特性:

持续学习和实践:

3、为什么要采用移动语义?

如何实现移动语义?

移动语义的实战应用

注意事项

总结

例:

代码分析

潜在陷阱

结论

4、返回值 vs 输出参数

返回值

输出参数

栗子

什么时候该用输出参数???

返回多个值???

5、指针 VS 引用

引用的特点:

指针的特点:

选择指针还是引用?


你是否曾凝视着代码,心中浮现出这些疑惑:

  1. 这个参数更适合按值传递还是按引用传递呢?
  2. 为何我的程序运行如此缓慢,是否与参数的传递方式有关?
  3. 移动语义背后究竟隐藏着怎样的奥秘?
  4. 在需要返回多个值时,是选择使用元组(tuple)还是结构体(struct)更为合适?

若有,那就继续Look Look... 

1、输入参数的传递方式-选择传值还是传引用?

        这是一个在C++编程中经常让人纠结的问题。让我们来探讨一下这个问题的黄金法则:

        当参数是“轻量级”的数据类型(例如int、指针等小型变量)时,选择传值是一个明智之举。这是因为传值操作直接且高效,无需担心指针解引用的额外开销。

        然而,当参数是“重量级”的数据类型(例如string这种大型对象)时,传const引用则显得更为合适。这是因为传const引用可以避免不必要的对象拷贝,从而节省时间和内存资源。

        那么,为什么会这样呢?原因如下:

        传值的好处在于其简单直接性,无需处理复杂的指针操作,减少了出错的可能性。

        而传const引用的好处则在于其效率性,特别是对于大型对象而言,避免了拷贝的开销,使得程序运行更加高效。

        

在处理不同类型的数据时,选择正确的参数传递方式至关重要。下面是对您提供的代码片段中参数传递方式的点评及优化建议:

处理用户信息

  • 推荐方式void processUserInfo(const string& name); 
    • 理由:字符串(string)在C++中通常是一个相对较大的数据结构,包含动态分配的内存。因此,通过const引用传递可以避免不必要的拷贝,提高程序效率。
  • 不推荐方式void processUserInfo(string name); 
    • 理由:这种方式会导致每次调用函数时都进行字符串的拷贝,增加了不必要的内存分配和释放操作,降低了程序性能。

处理坐标

  • 推荐方式void movePoint(int x, int y); 
    • 理由:整数(int)是基本数据类型,占用内存小,传值操作高效且直接。
  • 不推荐方式void movePoint(const int& x, const int& y); 
    • 理由:对于基本数据类型,使用引用传递反而会增加额外的开销(如指针解引用),而且整数本身就很小,拷贝的开销几乎可以忽略不计。

处理配置

  • 推荐方式void updateConfig(const vector<int>& config); 
    • 理由vector<int>可能包含大量的整数元素,占用较大的内存空间。通过const引用传递可以避免整个向量的拷贝,提高程序效率。
  • 不推荐方式void updateConfig(vector<int> config); 
    • 理由:这种方式会导致每次调用函数时都进行向量的拷贝,包括动态内存分配和元素复制,开销很大。

处理ID

  • 推荐方式void processId(int id); 
    • 理由:ID通常是整数类型,占用内存小,传值操作高效且直接。
  • 不推荐方式void processId(const int& id); 
    • 理由:同样地,对于基本数据类型(如整数ID),使用引用传递是不必要的,因为整数本身就很小,拷贝的开销几乎可以忽略不计。

        综上所述,在选择参数传递方式时,应根据数据类型的大小和特性来决定。对于大型数据结构(如字符串、向量等),使用const引用传递可以避免不必要的拷贝;而对于小型数据结构(如基本数据类型),则可以直接传值。

2、对于需要修改的参数,使用非const引用传递

        当需要修改函数内部的参数时,我们应选择使用非const引用来传递它。这样做的原因何在呢?让我们通过一个生动的比喻来揭示其重要性。

        想象一下,你有一把珍贵的钥匙,需要交给朋友去开门。但你不确定他是否会私自复制这把钥匙。这种不确定性就像你把一个参数传给函数时,却不清楚它是否会在函数内部被修改。

        为了避免这种潜在的风险,使用非const引用就像是给参数加上一个醒目的标签:“注意这个参数我可能会在函数内部进行修改哦!”这样一来,代码的意图就变得清晰明了,任何阅读你代码的人都能立刻明白这个参数的用途和可能的变化。

        非const引用的这种明确性不仅提高了代码的可读性,还增强了代码的安全性。它确保了函数内部对参数的修改是可控和预期的,从而避免了因参数被意外修改而导致的潜在错误。

        因此,在需要修改函数内部参数的情况下,选择非const引用来传递参数是一个明智且安全的选择。

        举例:


// result命名不明确,无法直观判断其用途
void calculate_sum(int values[], int count, int* result); 

// sum作为输出参数,明确表示其将被函数修改以存储结果
void calculate_sum(const int values[], int count, int& sum);

// 使用char*类型的str作为参数,其修改状态不明确
void parse_name(char* str);

// 使用string引用作为参数,直观表达其将被修改
void parse_name(std::string& str);

        有趣的例外:警惕表象的迷惑

        有些数据类型看似温和无害,实则暗藏玄机,能在不经意间修改原始对象。换句话说,就是有些类型表面上看起来安全无害,但实际上它们具有修改原始对象的能力,这种能力往往不易被察觉。

#include <vector>
#include <memory>

class Widget {
    std::vector<int> data;
public:
    void add(int x) { data.push_back(x); }
};

// process函数接收一个shared_ptr<Widget>的按值传递参数
// 但由于shared_ptr管理的是一个动态分配的对象,
// 因此通过解引用shared_ptr(即w->add(42))可以修改该对象的状态
void process(std::shared_ptr<Widget> w) 
{
    w->add(42);  // 这里的操作看似按值传递,但实则修改了w所指向的Widget对象
}

// update_iterator函数接收一个vector<int>::iterator的按值传递参数
// 迭代器本质上是一个指向vector中元素的指针封装
// 因此,通过解引用迭代器(即*it = 100)可以修改vector中对应元素的值
void update_iterator(std::vector<int>::iterator it)
{
    *it = 100;  // 这里的操作虽然也是按值传递迭代器,但实则修改了迭代器所指向的vector元素
}

详细解释

  1. process函数
    • 参数w是一个std::shared_ptr<Widget>的按值传递实例。
    • 尽管w是按值传递的,但它所管理的Widget对象是动态分配的,并且多个shared_ptr实例可能共享对该对象的所有权。
    • 通过w->add(42),我们实际上是在修改w所指向的Widget对象的状态,向其data成员添加了一个新元素。
    • 因此,尽管w本身是按值传递的,但其所指向的对象的状态是可以被修改的。
  2. update_iterator函数
    • 参数it是一个std::vector<int>::iterator的按值传递实例。
    • 迭代器it实际上是一个封装了指向vector<int>中某个元素的指针的对象。
    • 通过*it = 100,我们解引用了迭代器,并修改了它所指向的vector<int>中的元素的值。
    • 因此,尽管it本身是按值传递的,但其所指向的vector<int>元素的值是可以被修改的。

总结

        在这两个场景中,尽管参数是按值传递的,但由于它们分别指向或封装了可以修改的对象或元素,因此仍然可以产生副作用。这提醒我们在编写代码时要格外小心,确保理解参数的实际含义和它们所指向或封装的内容。

需要警惕的陷阱 

        引用参数,这一编程中的利器,犹如一把双刃剑,既能够作为输入传递数据,又能够作为输出返回结果。然而,若使用不当,这把剑也可能反过来伤到自己,带来意想不到的问题。

        在编程实践中,引用参数因其能够直接修改原始数据而备受青睐。这种特性使得函数能够高效地处理大型数据结构,而无需进行繁琐的数据复制。然而,正是这种直接修改的能力,也带来了潜在的风险。

        当函数通过引用参数接收数据时,调用者可能并不清楚数据会在函数内部被如何修改。如果函数内部对数据进行了意外的修改,那么调用者的数据状态也可能随之发生变化,从而导致程序行为异常。

        此外,即使函数本身没有意图修改数据,但由于引用参数的存在,其他函数或代码段也可能通过该引用间接地修改数据。这种隐蔽的数据修改方式往往难以追踪和调试,从而增加了程序的复杂性和维护成本。

        因此,在使用引用参数时,我们需要格外小心。要明确函数对引用参数的修改意图,并在文档中清晰地说明这一点。同时,也要谨慎地选择是否使用引用参数,以避免不必要的复杂性和潜在的风险。

        总之,引用参数是一把双刃剑,既能带来便利,也可能带来麻烦。只有在使用时保持警惕和谨慎,才能确保程序的正确性和稳定性。

“糟糕”的update方法

void update(Person& p) { 
    *this = p;  // 危险操作:完全替换了当前对象
}

        此方法名为update,但实际操作却如同“夺舍”——它完全用传入的Person对象p替换了当前对象的状态。这种操作极具破坏性,因为它不仅更改了对象的所有属性,还忽略了调用者可能只想更新部分属性的需求。此外,若p是一个临时对象或即将被销毁的对象,这种替换可能导致资源泄露或悬挂指针等潜在问题。

“完美”的set_nameset_age方法

void set_name(const string& new_name) { name_ = new_name; }

void set_age(int new_age) { 
    if (new_age < 0) throw invalid_argument("年龄不能为负,你想穿越吗?");
    age_ = new_age; 
}

        相比之下,set_nameset_age方法则如同两位专业的美容师,它们温柔地、有选择地更新对象的属性。set_name方法仅修改name_属性,而set_age方法不仅修改age_属性,还在赋值前进行有效性检查,确保年龄不会设为负数。这种方法既安全又灵活,因为它允许调用者精确地控制哪些属性需要更新。

总结与最佳实践

  • 避免使用“夺舍”式的update方法:它们破坏了对象的封装性,可能导致不可预测的状态变化。
  • 采用“美容师”式的属性设置方法:通过提供明确的、有选择性的属性设置方法(如set_nameset_age),可以更好地控制对象的状态变化,并确保属性的有效性。
  • 遵循最小权限原则:仅暴露必要的接口给外部使用,以减少潜在的安全风险和维护成本。

        通过遵循这些最佳实践,可以创建更加健壮、可维护和安全的代码。

如何避免掉进这些坑?

要避免掉进编程中的陷阱,特别是与引用参数相关的陷阱,我们可以依靠编译器的帮助,并遵循一些最佳实践。以下是一些建议,帮助你编写更加健壮和清晰的代码:

  1. 利用编译器的警告和错误

    • 现代编译器非常智能,它们能够识别出许多潜在的代码问题。例如,如果你声明了一个非const引用参数但在函数体内没有修改它,一些编译器可能会给出警告,提示你可能忘记了修改或者这个参数应该被声明为const
    • 同样,如果你不小心对一个非const引用参数执行了移动操作(std::move),编译器也可能会警告你这样做可能会导致数据丢失或未定义行为。
  2. 遵循const正确性

    • 默认情况下,应将函数参数声明为const引用,除非你确实需要在函数内部修改它们。这有助于保护数据不被意外修改,并增加代码的可读性和可维护性。
    • 如果函数不需要修改参数,使用const引用可以避免不必要的复制,同时保证数据的安全性。
  3. 明确函数意图

    • 通过函数命名和文档清晰地表达函数的意图。如果函数旨在修改参数,那么使用非const引用是合适的。但是,如果函数只是读取参数,则应使用const引用。
  4. 使用std::optional或返回值

    • 如果函数有时需要修改参数,有时又不需要,可以考虑使用std::optional<T&>(在C++17及更高版本中可用)来明确指示哪些参数可能被修改。
    • 另一种方法是让函数返回一个新的对象或值,而不是修改输入参数。这有助于保持函数的纯净性(即无副作用)。
  5. 代码审查和测试

    • 定期进行代码审查,让团队成员相互检查代码,以发现潜在的错误和陷阱。
    • 编写单元测试来验证函数的行为,确保它们按预期工作。
  6. 了解编译器的特性

    • 不同的编译器可能有不同的警告和错误消息。了解你所使用的编译器的特性,并启用尽可能多的警告选项,可以帮助你发现更多的问题。
  7. 持续学习和实践

    • 编程是一个不断学习和实践的过程。通过阅读文档、参加培训课程、参与社区讨论等方式,不断提高自己的编程技能。

        记住,编译器是我们的好朋友,但它并不能捕捉到所有的错误。因此,我们还需要依靠良好的编程习惯、代码审查和测试来确保代码的质量和可靠性。遵循这些建议,你将能够避免许多常见的陷阱,并编写出更加健壮和清晰的代码。

在C++的舞台上,"移动"对象这出戏确实是一场精彩绝伦的表演。它关乎于如何高效地传递大型对象,避免不必要的复制,从而提升程序的性能。接下来,我们就来深入探讨一下这场表演的艺术所在。

3、为什么要采用移动语义?

        在C++中,当我们需要传递或返回大型对象时,如果采用值传递的方式,编译器会生成该对象的副本。对于大型对象而言,这个过程可能会消耗大量的时间和内存。然而,很多时候,我们并不需要在源位置保留这个对象,此时,移动语义就派上了用场。

        移动语义允许我们“偷走”对象的资源(如动态分配的内存、文件句柄等),而不是复制它们。这样,目标对象就可以接管这些资源,而源对象则变为一个有效但未定义状态(通常称为“空”或“已移动”状态)。这种方式可以极大地提高性能,特别是在处理大型数据结构时。

如何实现移动语义?

        在C++中,实现移动语义通常需要以下两个步骤:

  1. 定义移动构造函数:这是一个特殊的构造函数,它接受一个右值引用(T&&)作为参数。右值引用是C++11引入的一种新特性,它允许我们区分左值(如变量、函数返回值等)和右值(如临时对象、std::move的返回值等)。移动构造函数会“偷走”传入对象的资源,而不是复制它们。

  2. 定义移动赋值运算符:这与移动构造函数类似,但它用于赋值操作。它同样接受一个右值引用作为参数,并“偷走”传入对象的资源。

移动语义的实战应用

        在标准库中,许多容器和字符串类都实现了移动语义。例如,std::vectorstd::string都提供了移动构造函数和移动赋值运算符。这使得我们可以高效地传递和返回这些类型的对象。

        此外,std::move函数也是一个非常重要的工具。它可以将一个左值强制转换为右值引用,从而允许我们调用移动构造函数或移动赋值运算符。但请注意,std::move本身并不移动任何东西;它只是改变了对象的值类别,使得移动操作成为可能。

注意事项

        虽然移动语义可以极大地提高性能,但我们也必须小心使用。一旦对象被移动,它的状态就不再有效。因此,在移动对象后,我们应该避免再使用它(除非我们明确知道它的新状态)。此外,移动语义还可能与对象的析构函数和资源管理策略相互作用,因此我们需要确保这些方面都得到妥善处理。

总结

        在C++的舞台上,"移动"对象这出戏确实是一场值得一看的表演。通过实现移动构造函数和移动赋值运算符,并合理使用std::move函数,我们可以高效地传递和返回大型对象,从而提升程序的性能。但请记住,移动语义是一把双刃剑;在使用它时,我们需要小心谨慎地处理对象的状态和资源管理问题。

例:

        在C++中,您所展示的代码片段巧妙地运用了移动语义来优化性能。然而,这里有一些细微之处和潜在陷阱需要注意。首先,让我们来分析一下代码:

string make_greeting(string&& name) {
    string result = "Hello, ";
    result += std::move(name); // 直接“偷”走name的内容
    return result; // result也会被移动返回,效率极高!
}

// 使用示例
string name = "Alice";
string greeting = make_greeting(std::move(name)); 
// 此时name变为空字符串,greeting则包含了完整的问候语

代码分析

  1. 函数签名string make_greeting(string&& name) 表明该函数接受一个右值引用到string类型的参数。这允许函数“偷”走传入string对象的资源,因为右值引用通常用于表示可以安全移动的对象。

  2. 移动操作:在函数体内,std::move(name)被用于将name的内容“转移”给result。这里,std::move并不真正移动数据,而是将name转换为右值引用,从而允许编译器选择移动赋值运算符(如果可用)来优化性能。

  3. 返回值:函数返回result,这是一个局部变量。在C++11及更高版本中,返回局部变量时,如果类型支持移动语义,编译器通常会使用命名返回值优化(NRVO)或移动语义来避免不必要的复制。然而,即使没有这个优化,std::string的拷贝构造函数和赋值运算符通常也被高度优化,因此这里的性能差异可能并不显著(除非string非常大或复制操作非常昂贵)。

  4. 调用后的状态:由于namestd::move转换并传递给make_greeting,它在函数返回后处于未定义状态(但在这个特定情况下,由于std::string的移动构造函数会清空源字符串,所以name变为空字符串)。这意味着在调用make_greeting后,不应再使用name

潜在陷阱

  • 重复移动:虽然在这个例子中std::move的使用是合理的,但在某些情况下,过度使用std::move可能会导致不必要的移动操作,从而损害性能。例如,如果name在传递给make_greeting之前或之后还需要被使用,那么就不应该对它使用std::move

  • 未定义状态:记住,在移动操作后,源对象(在这个例子中是name)处于未定义状态。因此,在移动之后使用它是不安全的。

  • 命名返回值优化(NRVO):虽然NRVO可能会减少或消除返回result时的复制或移动操作,但这不是程序员可以依赖的行为。它取决于编译器的实现和优化级别。

结论

您的代码示例展示了如何在C++中使用移动语义来优化性能。然而,在实际编程中,需要谨慎使用std::move和移动语义,以避免潜在的陷阱和未定义行为。同时,也要意识到现代C++编译器在优化返回值方面已经做得非常出色,因此有时不必过度担心性能问题。

4、返回值 vs 输出参数

返回值

优点

  1. 直观性:这种方法非常直观,因为函数直接返回所需的结果。调用者无需准备额外的变量来接收结果。

  2. 简洁性:代码更加简洁,因为不需要额外的参数来传递输出。

  3. 链式调用:返回值允许链式调用,即一个函数的返回值可以作为另一个函数的参数。

缺点

  1. 大型对象:对于大型对象或复杂数据结构,返回值的开销可能较大,因为可能需要复制或移动数据。

  2. 错误处理:如果函数可能失败并需要报告错误,返回值可能变得复杂,特别是当成功和失败的情况都需要返回不同类型的数据时。

输出参数

优点

  1. 避免复制:对于大型对象,使用输出参数可以避免复制或移动数据,因为数据是直接在调用者提供的变量中修改的。

  2. 灵活性:输出参数允许函数返回多个结果,或者通过引用修改输入参数。

  3. 错误处理:在某些情况下,输出参数可以更容易地处理错误,例如通过引用传递一个错误代码或状态。

缺点

  1. 不直观:对于不熟悉的人来说,输出参数可能不太直观,因为它们需要额外的变量来接收结果。

  2. 易出错:如果调用者忘记提供有效的输出参数(例如,传递了一个空指针或未初始化的引用),则可能导致未定义行为或崩溃。

  3. 链式调用受限:输出参数通常不支持链式调用,因为函数不返回任何有用的结果(或只返回一个状态码)。

        在实际编程中,选择返回值还是输出参数取决于具体的情况和需求。对于小型、简单的数据结构,返回值通常是更好的选择,因为它更直观且易于使用。对于大型、复杂的数据结构或需要返回多个结果的情况,输出参数可能更合适,因为它可以避免不必要的复制和移动操作,并提供更大的灵活性。

栗子

#include <iostream>

int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 5;
    int y = 10;
    int sum = add(x, y);
    std::cout << "Sum (return value): " << sum << std::endl;
    return 0;
}
#include <iostream>

void add(int a, int b, int& result) {
    result = a + b;
}

int main() {
    int x = 5;
    int y = 10;
    int sum;
    add(x, y, sum);
    std::cout << "Sum (output parameter): " << sum << std::endl;
    return 0;
}

什么时候该用输出参数???

        在C++编程中,选择是否使用输出参数(通常通过引用或指针传递)取决于多个因素,包括函数的目的、需要返回的数据量、以及是否希望修改传入的参数等。以下是一些指导原则,帮助你在自定义函数中决定何时使用输出参数:

  1. 需要修改传入的参数
    如果函数需要修改其传入的参数,并且这些修改对调用者是有意义的,那么应该使用输出参数。例如,一个排序函数可能需要传入一个数组,并对其进行原地排序。

  2. 返回多个值
    当函数需要返回多个值时,使用输出参数是一种常见的方法。C++函数只能有一个返回值,但如果有多个结果需要返回给调用者,那么可以通过输出参数来实现。

  3. 避免不必要的复制
    对于大型对象或数据结构,如果通过返回值传递会导致不必要的复制,那么使用输出参数可能更高效。通过引用或指针传递可以避免复制,从而节省时间和内存。

  4. 保持接口的一致性
    如果函数的接口需要与现有的API或库保持一致,并且这些接口已经使用了输出参数,那么为了保持一致性,你的函数也应该使用输出参数。

  5. 函数的目的和语义
    考虑函数的目的和它所表达的语义。有时候,使用输出参数可以使函数的意图更加清晰。例如,一个查找函数可能通过输出参数返回找到的元素的索引,并通过返回值表示是否找到了该元素。

  6. 错误处理和异常安全
    使用输出参数有时可以更容易地处理错误和异常。例如,如果函数在执行过程中遇到错误,它可以通过输出参数返回部分结果或状态信息,同时设置错误代码或抛出异常。

  7. 调用者的便利性
    考虑调用者的便利性。有时候,使用输出参数可以使调用者的代码更加简洁和直观。例如,如果调用者已经有一个变量来存储结果,并且希望直接更新这个变量,那么使用输出参数可能更方便。

        然而,也需要注意过度使用输出参数可能会导致函数接口变得复杂和难以理解。因此,在决定使用输出参数时,应该权衡其优缺点,并确保函数接口的设计是清晰、直观和易于使用的。

返回多个值???

        在C++中,当你需要从一个函数返回多个值时,有几种推荐的方式可以考虑。以下是一些常见的方法:

  1. 使用结构体或类
    定义一个结构体或类来封装需要返回的多个值。这种方法具有良好的可读性和类型安全性。

    struct Result {
        int value1;
        double value2;
        std::string message;
    };
    
    Result getValues() {
        Result result;
        result.value1 = 42;
        result.value2 = 3.14;
        result.message = "Success";
        return result;
    }
  2. 使用std::tuple
    std::tuple是C++11引入的一个模板类,用于存储固定大小的异构值集合。它提供了一种轻量级的方式来返回多个值,而不需要定义新的结构体或类。

    #include <tuple>
    #include <string>
    
    std::tuple<int, double, std::string> getValues() {
        return std::make_tuple(42, 3.14, "Success");
    }
    
    // 使用时,可以通过std::get来获取值
    int main() {
        auto [value1, value2, message] = getValues(); // C++17结构化绑定
        // 或者在C++11/14中使用std::get
        // int value1 = std::get<0>(getValues());
        // double value2 = std::get<1>(getValues());
        // std::string message = std::get<2>(getValues());
        return 0;
    }

    注意:在C++17中,你可以使用结构化绑定来简化从std::tuple中提取值的语法。

  3. 使用输出参数
    通过引用或指针将值作为参数传递给函数,并在函数内部进行赋值。这种方法适用于需要修改传入参数或避免复制大型对象的情况。

    void getValues(int& value1, double& value2, std::string& message) {
        value1 = 42;
        value2 = 3.14;
        message = "Success";
    }
    
    int main() {
        int value1;
        double value2;
        std::string message;
        getValues(value1, value2, message);
        return 0;
    }

    然而,这种方法可能会使函数签名变得复杂,并且需要调用者准备适当的变量来接收输出。

  4. 使用std::pair
    如果只需要返回两个值,并且这两个值的类型不同,那么可以使用std::pair

    #include <utility> // 包含std::pair
    
    std::pair<int, double> getValues() {
        return std::make_pair(42, 3.14);
    }
    
    int main() {
        auto [value1, value2] = getValues(); // C++17结构化绑定
        // 或者在C++11/14中使用std::get(但这里不适用,因为std::pair没有std::tuple的std::get函数,应该直接访问first和second)
        // int value1 = getValues().first;
        // double value2 = getValues().second;
        return 0;
    }

    注意:尽管std::pair只能存储两个值,并且这两个值的类型在编译时是固定的,但它比std::tuple更轻量,并且在只需要返回两个值的情况下更为方便。

        总的来说,选择哪种方法取决于你的具体需求、代码的可读性和维护性考虑。对于返回多个异构值的情况,std::tuple和结构体/类是很好的选择。如果你只需要返回两个值,并且这两个值的类型不同,那么std::pair可能是一个更简单的选择。输出参数适用于需要修改传入参数或避免复制大型对象的情况,但可能会使函数签名变得复杂。

5、指针 VS 引用

在C++编程中,指针和引用都是用于间接访问变量的工具,但它们有不同的使用场景和特性。选择使用指针还是引用,通常取决于你的具体需求、代码的可读性、安全性以及潜在的性能考虑。

引用的特点:

  1. 安全性:引用在创建时必须被初始化,且之后不能再改变指向。这有助于避免悬挂指针(dangling pointer)和野指针(wild pointer)等问题。

  2. 语法简洁:使用引用可以使代码更加简洁和直观,因为它们提供了类似变量的语法。

  3. 无法为空:引用不能为空(null),这意味着你不能创建一个未初始化的引用。这有时是一个优点,因为它可以强制你在使用引用之前对其进行初始化。

  4. 不支持算术运算:引用不支持指针算术运算,这有助于防止一些常见的指针错误。

指针的特点:

  1. 灵活性:指针可以指向任何有效的内存地址,包括空指针(null)。这使得指针在需要表示“无值”或“未初始化”状态时非常有用。

  2. 动态内存管理:指针常用于动态内存分配和释放,这在需要处理大量数据或创建复杂数据结构时非常有用。

  3. 支持算术运算:指针支持算术运算,如加法、减法和比较,这使得它们在处理数组和内存块时非常有用。

  4. 风险更高:由于指针的灵活性,它们也更容易出错。例如,悬挂指针、野指针、内存泄漏和缓冲区溢出等问题通常与指针的使用有关。

选择指针还是引用?

  1. 如果你需要一个必须被初始化的变量别名:使用引用。引用在创建时必须被初始化,且之后不能改变指向,这有助于避免一些常见的错误。

  2. 如果你需要处理可能为空的情况:使用指针。引用不能为空,而指针可以。

  3. 如果你需要动态分配内存:使用指针。虽然可以使用引用来引用动态分配的对象,但指针在内存管理方面提供了更多的灵活性。

  4. 如果你需要处理数组或内存块:使用指针。指针支持算术运算,这使得它们在处理数组和内存块时非常有用。

  5. 如果你希望代码更加简洁和直观:在可能的情况下使用引用。引用的语法更接近于普通变量,这使得代码更易于阅读和理解。

  6. 如果你需要传递大型对象:考虑使用引用或指针来避免不必要的复制。然而,请注意,对于小型对象,复制可能更高效,因为复制的开销可能小于传递指针或引用的开销(包括可能的间接访问和缓存未命中)。

总的来说,选择使用指针还是引用取决于你的具体需求。在可能的情况下,优先考虑使用引用,因为它们提供了更好的安全性和语法简洁性。然而,在某些情况下,指针的灵活性和动态内存管理能力可能是必要的。

        PS:这些都是浮云,代码能跑就行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/911836.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Eclipse系列】eclipse安装与常规配置(含插件)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、下载与安装 二、常规设置 1.1.设置工作空间(workspace) 1.2.设置字体和字体大小 ​编辑 1.3.设置编码 1.4.去除验证(validation) 1.5.去除单词验证(spelli…

内网对抗-信息收集篇SPN扫描DC定位角色区域定性服务探针安全防护凭据获取

知识点&#xff1a; 1、信息收集篇-网络架构-出网&角色&服务&成员 2、信息收集篇-安全防护-杀毒&防火墙&流量监控 3、信息收集篇-密码凭据-系统&工具&网站&网络域渗透的信息收集&#xff1a; 在攻防演练中&#xff0c;当完成边界突破后进入内…

C语言 | Leetcode C语言题解之第540题有序数组中的单一元素

题目&#xff1a; 题解&#xff1a; int singleNonDuplicate(int* nums, int numsSize) {int low 0, high numsSize - 1;while (low < high) {int mid (high - low) / 2 low;mid - mid & 1;if (nums[mid] nums[mid 1]) {low mid 2;} else {high mid;}}return …

【开发】关于Java中String与Integer的小小知识点(使用等号对比引用对象)

一个很简单的小知识点 我们都知道&#xff0c;如果使用对比包装类型或对象&#xff0c;那么比较的都是两者之间的地址&#xff08;指针或句柄&#xff09;&#xff0c;而非对象本身&#xff0c;那么且看下方的代码。 public class A {public static void main(String[] args)…

纯前端实现在线预览excel文件(插件: LuckyExcel、Luckysheet)

概述 在实际开发中&#xff0c;遇到需要在线预览各种文件的需求&#xff0c;最近遇到在线预览excel文件的需求&#xff0c;在此记录一下&#xff01;本文主要功能实现&#xff0c;用于插件 LuckyExcel &#xff0c;Luckysheet&#xff01;废话不多说&#xff0c;上代码&#xf…

洛谷每日一题——B2143 进制转换、P1003 [NOIP2011 提高组] 铺地毯

B2143 进制转换 题目描述 进制转换 - 洛谷 运行代码 #include<stdio.h> int main(){int a,b,i0,j,num[20];char k[]{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F};scanf("%d",&a);scanf("%d",&b);do{i;num[i]a%b;aa/b;}while(a!0);printf("&qu…

Elasticsearch的自定义查询方法到底是啥?

Elasticsearch主要的目的就是查询&#xff0c;默认提供的查询方法是查询全部&#xff0c;不满足我们的需求&#xff0c;可以定义查询方法 自定义查询方法 单条件查询 我们查询的需求&#xff1a;从title中查询所有包含"鼠标"这个分词的商品数据 SELECT * FROM it…

环境配置与搭建

安装pytorch 官网连链接&#xff1a;https://pytorch.org/ 特殊包名 cv2 pip install opencv-python sklearn pip install scikit-learnPIL pip install Pillow使用jupyter notebook pip install jupyter安装显卡驱动 Windows Linux 视频教程&#xff1a; 【ubuntu2…

jmeter常用配置元件介绍总结之函数助手

系列文章目录 1.windows、linux安装jmeter及设置中文显示 2.jmeter常用配置元件介绍总结之安装插件 3.jmeter常用配置元件介绍总结之取样器 jmeter常用配置元件介绍总结之函数助手 1.进入函数助手对话框2.常用函数的使用介绍2.1.RandomFromMultipleVars函数2.2.Random函数2.3.R…

【excel基本操作-sumif绝对引用和相对引用

低量级数据的存储 复杂且无法优化的数据报表 怎么学excel? 一、输入与输出 二、计算与处理 三、可视化 四、连接匹配与自动化 excel操作笔记 打开表格第一步筛选 所以筛选的快捷键&#xff1a;shiftctrll 排序&#xff1a;多列排序 开始-排序与筛选-自定义排序-设置关键字添…

【项目计划文档】软件项目计划书,项目总体计划(word原件)

项目开发计划包括项目描述、项目组织、成本预算、人力资源估算、设备资源计划、沟通计划、采购计划、风险计划、项目过程定义及项目的进度安排和里程碑、质量计划、数据管理计划、度量和分析计划、监控计划和培训计划等。 软件全套精华资料包清单部分文件列表&#xff1a; 工作…

音视频入门基础:FLV专题(23)——FFmpeg源码中,获取FLV文件音频信息的实现(下)

音视频入门基础&#xff1a;FLV专题系列文章&#xff1a; 音视频入门基础&#xff1a;FLV专题&#xff08;1&#xff09;——FLV官方文档下载 音视频入门基础&#xff1a;FLV专题&#xff08;2&#xff09;——使用FFmpeg命令生成flv文件 音视频入门基础&#xff1a;FLV专题…

模型 阿玛拉定律(炒作周期)

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。短期乐观&#xff0c;长期低估。 1 阿玛拉定律的应用 1.1 全球定位系统&#xff08;GPS&#xff09;的发展 全球定位系统&#xff08;GPS&#xff09;的发展是阿玛拉定律的一个典型应用案例&#xf…

Kubernetes的概述与架构

Kubernetes 的概述 Kubernetes 是一个可移植、可扩展的开源平台&#xff0c;用于管理容器化的工作负载和服务&#xff0c;方便进行声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统&#xff0c;其服务、支持和工具的使用范围广泛。 Kubernetes 这个名字源于…

【CAN通信】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、CAN通信简介二、CAN通信的逻辑电平分析三、CAN通信的差分信号线设计CAN标准数据帧格式四、设备发送数据优先级总结 一、CAN通信简介 CAN&#xff08;Controlle…

速度快还看巡飞,筒射巡飞无人机技术详解

筒射巡飞无人机&#xff08;Launch and Recovery by Tube&#xff0c;LRAT或Launcher-Deployed Loitering Munition&#xff0c;LDLM&#xff09;作为一种新型无人机系统&#xff0c;近年来在军事和民用领域都展现出了巨大的潜力。以下是对筒射巡飞无人机技术的详细解析&#x…

如何使用 SSH 连接并管理你的 WordPress 网站

在当今数字化的世界里&#xff0c;网站的管理与维护至关重要。对于使用 WordPress 搭建网站的用户而言&#xff0c;掌握基本的 SSH&#xff08;安全壳&#xff09;命令能够极大地简化网站管理工作。本指南将向你介绍 SSH 的基本知识&#xff0c;并教你如何通过 SSH 连接和管理你…

低轨卫星互联网(二)—— 技术篇

撰写:我是吉米 低轨卫星互联网,地面移动网和卫星通信网融合如图1所示。 图1 低轨卫星互联网演进图 从技术演进发展初衷来看,地面移动网与卫星通信网各自演进,如图2所示。地面移动网旨在提供高速率、大容量、低时延、高移动服务,而卫星通信网则旨在提供广覆盖服务。低轨卫星…

51c大模型~合集18

我自己的原文哦~ https://blog.51cto.com/whaosoft/11621494 #SpatialBot 空间大模型&#xff1a;上交、斯坦福、智源、北大、牛津、东大联合推出&#xff01; 大模型走向空间智能、具身智能之路&#xff01; 智源&#xff0c;斯坦福&#xff0c;北大&#xff0c;牛津&…

OpenGL 异常处理-glCreateShader失败

【1】glCreateShader创建顶点着色器时候报错&#xff0c;如下 【2】原因分析 初始化失败&#xff0c;你使用一个扩extension loader library来访问现代OpenGL&#xff0c;当需要初始化它时&#xff0c;加载器需要一个当前的上下文来加载 【3】解决办法 GLenum glew_err gle…