从C语言到C++(五)

自动类型推导

在C++中,自动类型推导(Automatic Type Deduction)主要通过auto关键字实现。当你使用auto关键字声明一个变量时,编译器会根据初始化该变量的表达式的类型来自动推断出变量的类型。

以下是一些使用auto进行自动类型推导的例子:

#include <iostream>
#include <vector>

int main() {
    // 基本类型推导
    auto a = 10;     // a的类型是int
    auto b = 3.14f;  // b的类型是float

    // 引用类型推导
    int x = 10;
    auto& ref_x = x; // ref_x的类型是int&,它是对x的引用

    // 指针类型推导
    int* ptr_x = &x;
    auto ptr_y = &x; // ptr_y的类型是int*

    // 复杂类型推导
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto first = v.begin(); // first的类型是std::vector<int>::iterator

    // 初始化列表推导
    auto c = {1, 2, 3}; // 在C++11中,这可能会推导出std::initializer_list<int>
    // 但如果auto与&结合使用,则可以推导出引用到数组
    auto& d = {1, 2, 3}; // 错误:不能对临时对象使用引用
    // 但可以这样使用:
    int arr[] = {1, 2, 3};
    auto& e = arr; // e的类型是int(&)[3],即arr的引用

    return 0;
}

注意

  1. 使用auto可以使代码更加简洁和清晰,但也可能导致可读性降低,特别是在复杂类型的情况下。因此,在使用auto时,需要权衡代码的可读性和简洁性。
  2. auto不会推导为引用类型,除非你明确使用&。同样,它也不会推导为指针类型,除非你明确使用*&运算符。
  3. auto不能用于函数参数或模板参数的类型推导。在这些情况下,你需要明确指定类型。
  4. 在C++17中,引入了结构化绑定(Structured Bindings),它允许你使用auto来同时声明多个变量,并从元组、对、结构体等中提取值。例如:auto [x, y] = std::make_pair(1, 2);

尾拖返回类型

在C++中,尾置返回类型(Trailing Return Type)或称为后置返回类型(Postfix Return Type)是一种在函数声明或定义中指定返回类型的语法特性,它特别有用于处理模板函数中返回类型依赖于模板参数的情况。尾置返回类型允许你在函数声明或定义的参数列表之后指定返回类型。

尾置返回类型的使用语法通常与auto关键字结合,并且使用->操作符来指定返回类型。以下是一个简单的例子,展示了如何在模板函数中使用尾置返回类型:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

在上面的例子中,decltype(t + u)就是尾置返回类型。它告诉编译器,这个函数的返回类型是tu相加的结果类型。

不过,从C++14开始,我们可以使用auto关键字直接在函数声明中推断返回类型,而不需要显式地使用尾置返回类型:

template<typename T, typename U>
auto add(T t, U u) {
    return t + u;
}

编译器会根据return语句中的表达式来推断函数的返回类型。这种自动类型推断使代码更加简洁和易读。

然而,在某些情况下,尾置返回类型仍然是必要的。例如,当返回类型依赖于多个参数,或者当返回类型是一个复杂的表达式,而不仅仅是函数参数的一个简单操作时,尾置返回类型就派上了用场。此外,它还可以用于解决某些类型推导问题,特别是当涉及引用折叠(reference collapsing)和std::forward等高级模板技术时。

类型信息推导

typeid

typeid是C++中的一个操作符,它用于在运行时获取一个类型或对象的实际类型信息。以下是关于typeid的详细解释:

1. 定义和基本作用
  • typeid是C++中的一个操作符,它用于获取一个类型或对象的运行时类型信息。
  • 在程序中,当我们需要获取某个对象或变量的类型信息时,可以使用typeid操作符。
  • typeid的返回值是一个type_info类型的对象,它包含了被查询对象的类型信息和一些相关函数和属性。
2. 使用方法
  • typeid类似于sizeof这样的操作符,但不是函数。
  • typeid定义在typeinfo头文件中。
  • 可以通过typeid(变量或类型).name()来获取类型的名称,但需要注意的是,不是所有编译器都会输出如"int"、"float"等这样的类型名称。
  • typeid可以用于动态类型,也可以用于静态类型。静态类型和动态类型分别对应的是编译时和运行时的类型识别。
  • typeid多数运用于class和继承中。
3. 注意事项
  • 对于非引用类型,typeid是在编译时期识别的;只有引用类型才会在运行时识别。
  • 运行时获知变量类型名称时,可以使用typeid(变量).name(),但需要注意返回的类型名称可能因编译器而异。
4. 示例代码
#include <iostream>
#include <typeinfo>
using namespace std;

int main(void) {
    int a;
    char b;
    unsigned char c;
    signed char d;
    cout << "a typeid =" << typeid(a).name() << endl; // 打印a的类型
    cout << "b typeid =" << typeid(b).name() << endl; // 打印b的类型
    cout << "c typeid =" << typeid(c).name() << endl; // 打印c的类型
    cout << "d typeid =" << typeid(d).name() << endl; // 打印d的类型
    return 0;
}
5. 关联概念:RTTI
  • typeid与RTTI(Run-Time Type Identification,运行时类型识别)紧密相关。RTTI使程序能够获取由基类指针或引用所指向的对象的实际派生类型。

总结:typeid是C++中用于在运行时获取类型信息的关键字,通过它我们可以获取一个类型或对象的实际类型信息,这在处理复杂的类型系统或进行类型检查和转换时非常有用。

decltype

decltype 是 C++11 引入的一个关键字,用于在编译时从表达式中推导类型。decltype 的主要作用是在编译时检查一个表达式并返回该表达式的类型,而不实际计算该表达式。这使得 decltype 在模板元编程、自动类型推导和函数返回类型推导等场景中特别有用。

基本用法

decltype 的基本语法如下:

decltype(expression) var;

这里 expression 是一个表达式,decltype 会根据这个表达式的类型来推导 var 的类型。

示例
  1. 基本类型推导
int x = 10;
decltype(x) y = 20; // y 的类型是 int
  1. 表达式推导
int a = 10, b = 20;
decltype(a + b) sum = a + b; // sum 的类型是 int
  1. 引用类型推导
int& ref = a;
decltype(ref) another_ref = b; // another_ref 是 int& 类型,它引用 b
  1. 函数返回类型推导
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

// 或者在 C++14 及以后使用自动返回类型推导
template<typename T, typename U>
auto add(T t, U u) {
    return t + u;
}

在上面的例子中,decltype(t + u) 用于推导函数 add 的返回类型,它依赖于参数 tu 的类型以及它们相加的结果类型。

注意事项
  • decltype 并不计算表达式的值,它只是检查表达式的类型。
  • decltype 用于未声明的变量或表达式时,编译器会报错。
  • 如果表达式是一个左值(如变量、数组元素、结构体的成员等),decltype 会推导出一个引用类型。如果表达式是一个右值(如字面量、临时对象等),则推导出的类型不是引用。
  • 可以通过添加括号来改变 decltype 的推导行为。例如,decltype((variable)) 总是推导出一个引用类型,即使 variable 是一个右值。
总结

decltype 是 C++ 中的一个强大工具,它允许程序员在编译时从表达式中推导类型,而无需显式指定。这使得代码更加灵活和易于维护,特别是在处理复杂类型和模板元编程时。

基于范围的增强for循环

在C++11及更高版本中,引入了基于范围的for循环(Range-based for loop),也被称为"for-each"循环,用于简化对容器(如数组、std::vectorstd::liststd::set等)或其他可迭代对象的遍历。

基于范围的for循环的语法如下:

for (declaration : range) {
    // 循环体
}

在这里,declaration是每次循环时从range中提取出的元素的声明,而range是一个可迭代的对象,比如一个容器。

下面是一些基于范围的for循环的示例:

示例 1:使用数组

#include <iostream>

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    for (int num : arr) {
        std::cout << num << ' ';
    }
    std::cout << std::endl;
    return 0;
}

示例 2:使用std::vector

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    for (int num : vec) {
        std::cout << num << ' ';
    }
    std::cout << std::endl;
    return 0;
}

示例 3:使用std::map

当遍历std::map时,你可以同时获得键和值。

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> myMap = {{"apple", 1}, {"banana", 2}, {"cherry", 3}};
    for (const auto& pair : myMap) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    return 0;
}

在这个例子中,pair是一个std::pair<const std::string, int>类型的对象,其中first是键,second是值。我们使用const auto&来避免不必要的拷贝,并提高性能。

基于范围的for循环在C++编程中非常有用,因为它使代码更简洁,同时仍然保持了很好的可读性。这种循环特别适合于只读迭代,当你不需要直接访问迭代器的时候。如果你需要修改迭代器(例如,在遍历过程中删除元素),那么你可能需要使用传统的迭代器循环。

函数

从C语言过渡到C++时,函数的概念在很多方面是相似的,但C++为函数提供了更多的特性和灵活性。以下是C和C++中函数的一些主要差异和C++特有的功能:

1. 函数重载(Overloading)

在C++中,函数重载(Function Overloading)是一种特性,它允许我们为同一个函数名定义多个版本,只要这些版本的参数列表(参数类型、参数数量或参数顺序)不同即可。编译器会根据调用时提供的参数类型和数量来确定调用哪个版本的函数。

下面是一个关于函数重载的例子:

#include <iostream>
#include <string>

// 第一个版本的print函数,接受一个整数参数
void print(int x) {
    std::cout << "Printing an integer: " << x << std::endl;
}

// 第二个版本的print函数,接受一个浮点数参数
void print(double x) {
    std::cout << "Printing a double: " << x << std::endl;
}

// 第三个版本的print函数,接受一个字符串参数
void print(const std::string& s) {
    std::cout << "Printing a string: " << s << std::endl;
}

// 第四个版本的print函数,接受两个整数参数
void print(int x, int y) {
    std::cout << "Printing two integers: " << x << " and " << y << std::endl;
}

int main() {
    print(10);           // 调用第一个版本的print函数
    print(10.5);         // 调用第二个版本的print函数
    print("Hello");      // 调用第三个版本的print函数
    print(10, 20);       // 调用第四个版本的print函数
    return 0;
}

在上面的例子中,我们定义了四个名为print的函数,每个函数都接受不同类型的参数或不同数量的参数。在main函数中,我们根据提供的参数类型和数量来调用不同版本的print函数。

函数重载必须满足以下条件:

  1. 函数名必须相同。
  2. 参数列表必须不同(可以是参数类型、参数数量或参数顺序不同)。
  3. 返回类型不参与函数重载的判定(即返回类型不同不能构成重载)。

注意:在C++中,函数重载是通过参数列表来区分的,而不是通过函数名或返回类型。因此,你不能仅通过改变函数名或返回类型来重载一个函数。

此外,还有一个需要注意的点是,当使用默认参数时,重载函数可能会产生歧义。例如,如果你有一个接受一个整数参数的函数和一个接受两个整数参数(其中第二个参数有默认值)的函数,那么只传递一个整数参数给这两个函数时,编译器可能无法确定要调用哪个函数。因此,在设计函数重载时要避免这种情况。

调用机制

在C++中,函数重载的调用机制主要依赖于函数的名称和参数列表(即参数的类型、数量和顺序)。当编译器遇到对某个函数的调用时,它会根据提供的参数来确定应该调用哪个重载版本。这个过程称为名称查找(Name Lookup)和重载解析(Overload Resolution)。

以下是重载函数调用机制的基本步骤:

  1. 名称查找(Name Lookup)

    • 编译器首先会在当前作用域中查找与调用名称匹配的函数声明。
    • 如果在当前作用域中找不到匹配的函数声明,编译器会继续在包含当前作用域的作用域中查找,直到找到全局作用域。
    • 这个过程会找到所有与调用名称匹配的函数声明,包括所有重载版本。
  2. 重载解析(Overload Resolution)

    • 一旦编译器找到了所有与调用名称匹配的函数声明,它就会开始重载解析过程。
    • 重载解析的主要任务是确定应该调用哪个重载版本的函数。这是通过比较调用时提供的参数与每个重载版本的参数列表来完成的。
    • 编译器会尝试将提供的参数与每个重载版本的参数列表进行匹配。匹配的过程包括类型转换(如果需要的话),但通常编译器会优先考虑那些不需要类型转换的匹配。
    • 如果某个重载版本的参数列表与提供的参数完全匹配(即不需要任何类型转换),那么这个版本就是最佳匹配,编译器将调用这个版本的函数。
    • 如果没有找到完全匹配的版本,编译器会尝试找到最接近的匹配。这通常涉及到类型转换和类型提升。编译器会考虑所有可能的类型转换,并选择一个“最佳”的匹配。
    • 如果存在多个同样“好”的匹配(例如,两个函数都需要相同的类型转换),编译器就会报错,因为这种情况下无法确定应该调用哪个函数。这被称为“重载解析失败”。
  3. 调用函数

    • 一旦确定了要调用的函数版本,编译器就会生成代码来调用该函数。这通常涉及到将参数传递给函数,并执行函数的代码。

需要注意的是,函数重载只与参数列表有关,与函数的返回类型无关。也就是说,你不能仅仅通过改变函数的返回类型来重载一个函数。此外,函数重载也与函数的定义位置无关,只要函数声明在调用之前可见即可。

另外,还需要注意的是,函数重载并不改变函数的名称或参数列表。它只是允许你使用相同的函数名来定义多个具有不同参数列表的函数。在编译时,编译器会根据提供的参数来确定应该调用哪个版本的函数。在运行时,函数重载对程序的行为没有任何影响。

2. 内联函数(Inline Functions)

C++支持内联函数,这是一种建议编译器将函数调用替换为函数体本身的机制。这可以减少函数调用的开销,但可能会增加代码大小。在C语言中,内联函数不是语言的一部分,但编译器可能提供特定的扩展来支持它。

// C++ 示例
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

内联函数的作用

内联函数(Inline Functions)在C++中主要起到以下作用:

  1. 减少函数调用的开销:当函数被声明为内联时,编译器会尝试在调用点将函数体直接插入,而不是进行常规的函数调用(即压栈、跳转、返回)。这样做可以消除函数调用的开销,从而提高程序的执行效率。对于小型且频繁调用的函数,这种开销减少尤为显著。

  2. 提高代码执行效率:由于内联函数在调用点直接插入函数体,可以减少因函数调用而产生的额外开销,如参数传递、栈帧创建和销毁等。因此,内联函数可以提高代码的执行效率。

  3. 消除函数调用的副作用:在某些情况下,函数调用可能会产生副作用,如修改全局变量或静态变量的值。而内联函数可以避免这种副作用,因为其在编译时展开,不会进行实际的函数调用。

  4. 支持类中的成员函数:在面向对象编程中,类的成员函数经常需要被频繁调用。将类的成员函数声明为内联函数可以减少因函数调用而产生的额外开销,从而提高程序的性能。此外,内联函数还可以保证类的封装性和隐藏性,因为只有在类的内部才能看到函数的实现细节。

  5. 提高可读性:对于某些简单的函数,将其声明为内联函数可以使代码更加紧凑和易于阅读。这是因为内联函数在调用点直接插入函数体,可以避免过多的函数调用和跳转,使代码结构更加清晰。

  6. 优化性能:在某些情况下,编译器可能会根据上下文和性能分析的结果自动将某些函数内联化。即使这些函数没有被显式声明为内联函数,编译器也会根据性能优化的需要进行内联展开。这进一步体现了内联函数在优化性能方面的作用。

需要注意的是,虽然内联函数可以提高程序的执行效率,但过度使用内联函数可能会导致代码膨胀和降低缓存效率。因此,在编写和使用内联函数时应该权衡利弊,根据具体情况进行选择。同时,也需要注意编译器对内联函数的支持程度和限制条件。

内联函数的限制

  1. 函数体复杂性

    • 内联函数的函数体不能含有复杂的结构控制语句,如whilefordo-whileswitch等循环和条件语句。如果内联函数中包含这些复杂的控制语句,编译器通常会将其视为普通函数处理,不进行内联展开。
    • 递归函数不能被声明为内联函数,因为递归函数需要函数调用栈的支持,而内联函数在编译时展开,不保留函数调用栈信息。
  2. 函数体大小

    • 内联函数通常适用于小型函数,如1-5行代码的小函数。过大的函数体可能会导致代码膨胀,降低程序的执行效率。
  3. 不能包含特定语句

    • 内联函数中不能说明数组(虽然这不是绝对的,但某些编译器可能会有限制)。
    • 内联函数中不能有过多的条件判断语句,因为这会降低内联的效果。
    • 不能对函数进行取址操作,因为内联函数没有独立的函数地址。
  4. 定义位置

    • 为了确保内联函数在所有调用它的地方都能被正确展开,通常需要将内联函数的定义放在头文件中。
  5. 编译器建议性

    • 内联函数是建议性的,而非强制性的。即使函数被声明为内联,编译器也有权选择忽略这个建议,不将函数内联化。这通常发生在函数体过大、过于复杂,或者编译器认为内联化不会带来性能提升的情况下。
  6. 性能考虑

    • 虽然内联函数可以减少函数调用的开销,但过度使用内联函数可能会导致代码膨胀,降低缓存命中率,反而降低程序的运行效率。因此,在使用内联函数时应该权衡利弊,根据具体情况进行选择。

综上所述,内联函数虽然可以提高程序的运行效率,但也有一些限制和需要注意的地方。在编写和使用内联函数时,应该根据具体情况进行权衡和选择。

3. 引用参数(Reference Parameters)

C++支持引用参数,允许函数直接操作传递给它的变量的原始数据,而不是其副本。这可以避免不必要的复制操作,提高效率。在C语言中,你只能通过指针来模拟这种行为。

// C++ 示例
void modify(int& x) { // 引用参数
    x = 10;
}

int main() {
    int y = 5;
    modify(y); // y 的值现在为 10
    return 0;
}

4. 默认参数(Default Parameters)

在C++中,可以为函数参数提供默认值。如果在调用函数时没有提供这些参数的值,则使用默认值。这在C语言中是不可能的。

// C++ 示例
void greet(std::string name = "World") {
    std::cout << "Hello, " << name << "!" << std::endl;
}

int main() {
    greet(); // 输出 "Hello, World!"
    greet("Alice"); // 输出 "Hello, Alice!"
    return 0;
}

默认参数

默认参数是C++中函数的一种特性,允许在函数调用时省略某些参数,此时将使用这些参数的默认值。以下是关于默认参数的详细解释:

  1. 定义与使用

    • 默认参数指的是在函数声明时给函数参数指定一个默认值。
    • 如果调用函数时没有给这个参数传入实参,则使用默认值;如果传入了实参,则替换掉默认值。
  2. 语法规则

    • 默认参数只能从最右侧变量依次赋值,即如果一个参数有默认值,则它右边的所有参数都必须有默认值。
    • 当函数的声明和实现分开时,需要在声明时指定默认值,实现时不能再次指定。
  3. 示例
    假设我们有一个函数print_int,它接受一个整型参数i,并有一个默认值-1

    void print_int(int i = -1) {
        std::cout << "i=" << i << std::endl;
    }
    

    调用这个函数时,我们可以选择是否传入参数:

    print_int();  // 输出:i=-1
    print_int(10); // 输出:i=10
    
  4. 注意事项

    • 不要过度依赖默认参数,因为它们可能会使函数的使用变得模糊,增加维护成本。
    • 当函数的参数较多,且很多参数都有默认值时,应该考虑使用其他方法(如结构体或类)来组织这些参数,以提高代码的可读性和可维护性。
  5. 优点

    • 提高了函数的灵活性,允许在函数调用时省略某些参数。
    • 可以简化函数调用,减少代码冗余。
  6. 缺点

    • 如果过度使用默认参数,可能会导致函数接口变得复杂和难以理解。
    • 如果在函数实现中再次指定了默认参数的值(当声明和实现分开时),将会导致编译错误。
  7. 与其他特性的关系

    • 默认参数可以与函数重载结合使用,以增加函数的多样性。
    • 内联函数也可以使用默认参数,但内联函数的主要目的是提高程序性能,而默认参数则主要用于提高函数的灵活性。

占位参数

定义函数时,还可以给函数提供占位参数

  • 占位参数只有参数类型,而没有参数名
  • 在函数体内部无法使用占位参数
  • 占位参数也可以指定默认参数
void func(int a,int = 0)
{
    cout<<a<<endl;
}
func(2);

5. 模板函数(Template Functions)

C++支持模板函数,允许你编写与类型无关的代码。编译器在编译时根据提供的类型信息实例化模板函数。这在C语言中是不可用的。

// C++ 示例
template <typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    int i = max(3, 5); // 调用 max<int>(3, 5)
    double d = max(3.14, 2.71); // 调用 max<double>(3.14, 2.71)
    return 0;
}

6. 成员函数(Member Functions)

在C++中,你可以定义类的成员函数,这些函数与类的特定实例相关联。这与C语言中的函数完全不同,C语言中的函数是全局的或静态的。

// C++ 示例
class MyClass {
public:
    int x;
    MyClass(int value) : x(value) {}

    // 成员函数
    void printValue() {
        std::cout << "x 的值为: " << x << std::endl;
    }
};

int main() {
    MyClass obj(10);
    obj.printValue(); // 输出 "x 的值为: 10"
    return 0;
}

这些只是C++函数相对于C语言函数的一些主要差异和新增功能。C++还提供了许多其他特性和功能,如异常处理、类和对象、继承、多态等,这些都使C++成为一种功能强大的编程语言。

相关推荐

  1. C语言C++(

    2024-06-16 21:38:03       28 阅读
  2. C语言C++(四)

    2024-06-16 21:38:03       28 阅读
  3. 【系统学C++】一、C语言C++(一)

    2024-06-16 21:38:03       30 阅读
  4. 【系统学C++】二、C语言C++(二)

    2024-06-16 21:38:03       30 阅读
  5. 【系统学C++】C语言C++(三)

    2024-06-16 21:38:03       25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-16 21:38:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-16 21:38:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-06-16 21:38:03       82 阅读
  4. Python语言-面向对象

    2024-06-16 21:38:03       91 阅读

热门阅读

  1. Git与SSH

    2024-06-16 21:38:03       34 阅读
  2. Vue3 和 Vue2 对比分析及示例代码解析(初级)

    2024-06-16 21:38:03       31 阅读
  3. Web前端高级课程:深入探索与技能飞跃

    2024-06-16 21:38:03       27 阅读
  4. 常见的中间件都在解决什么问题?

    2024-06-16 21:38:03       30 阅读
  5. 大数据数仓30问

    2024-06-16 21:38:03       28 阅读
  6. rsync常用命令

    2024-06-16 21:38:03       26 阅读
  7. vue2 + echats macarons 选中样式的树状图

    2024-06-16 21:38:03       31 阅读