您现在的位置是:首页 >技术交流 >【C/C++】语言相关题型(一)网站首页技术交流

【C/C++】语言相关题型(一)

Ricky_0528 2024-06-18 12:01:02
简介【C/C++】语言相关题型(一)

1. C/C++中的类型转换

1.1 C中的类型转换

在C语言中,可以用以下两种方式进行显式类型转换:

  1. (T) exp:在这种方式中,你将把表达式(exp)转换成类型T。这种类型转换方法被称为 “C风格的类型转换” 或者 “传统的类型转换”。

  2. T(exp):在这种方式中,你也将把表达式(exp)转换成类型T。这种类型转换方法在C++中更常见,并被称为 “函数风格的类型转换” 或者 “构造函数的类型转换”。

然而,这两种方式在C语言中的效果是一样的,即将表达式exp的类型转换成T。例如,如果你有一个整数 int i = 10; 并且你想将它转换为浮点数,你可以这样做: (float) i 或者 float(i)

但是需要注意,虽然这两种方式在C语言中都可以使用,但是函数风格的类型转换T(exp)在C++中更常见。如果你正在编写的是C程序,那么建议使用(T) exp,因为这种方式在所有的C编译器中都可以被接受。

最后,我想强调的是,无论是哪种类型转换方式,都应该谨慎使用。在不同类型之间进行转换可能会导致数据丢失或者精度损失。例如,将浮点数转换为整数,小数部分会被丢弃;而如果将大的整数转换为小的整数类型,可能会导致数据溢出。因此,在编写程序时,一定要确保你明白为什么需要进行类型转换,以及转换可能会带来什么样的影响。

1.2 C++中的类型转换

在C++中,提供了四种显式类型转换运算符,分别是 static_castconst_castdynamic_castreinterpret_cast。每一种类型转换运算符都有其特定的使用场景和规则。

  1. static_cast<T>(exp)
    这是最常见的类型转换方法,能够在任何数据类型之间进行转换,包括指针类型。然而,它不能将const对象转换为非const对象。除此之外,当你对指针进行static_cast时,必须确保转换的类型是安全的,因为这种类型转换没有运行时类型检查。

    • 类层次间转换
      • 上行转换是安全的
      • 下行转换不安全,没有动态类型检查
    • 基本类型转换
    • 空指针转换为目标类型的空指针
    • non-const转换为const
    • 局限:不能去掉const、volitale等属性
  2. const_cast<T>(exp)
    const_cast主要用于修改类型的const或volatile属性。比如,它可以将一个const指针转换为非const指针,或者将const对象转换为非const对象。需要注意的是,const_cast并不能改变底层数据,只是改变了对数据的访问权限。

    • 去掉对象指针或对象引用const属性
    • 目的:修改指针(引用)的权限,可以通过指针或引用修改某块内存的值
  3. dynamic_cast<T>(exp)
    dynamic_cast主要用于安全地向下转型(在继承层次中从基类向派生类转型)。也就是说,它可以在运行时进行类型检查,如果转换是非法的,那么转换的结果将会是nullptr。但这种类型转换只能用于含有虚函数的类,因为只有这样的类才能进行运行时类型检查。

    • 用于多态,在运行时进行类型转换
    • 在一个类层次结构中安全地类型转换,把基类指针(引用)转换为派生类指针(引用)
    • 因为引用不存在空引用,转换失败会抛出bad_cast异常,指针转换失败返回的是nullptr
  4. reinterpret_cast<T>(exp)
    reinterpret_cast是最不安全的类型转换方法,它能进行任意类型之间的转换,包括指针和整数之间的转换。然而,使用reinterpret_cast可能会产生非法的数据,所以除非在必要的时候才应该使用。

    • 改变指针(引用)类型
    • 将指针(引用)转换为一个整型
    • 将整型转换为指针(引用)
    • T必须为指针、引用、整型、函数指针、成员指针
    • 注意仅仅是比特位的拷贝,没有安全检查

1.3 例子

在C语言中,常见的类型转换主要是数值类型之间的转换。例如:

int i = 10;
double d = (double)i;  // 将整数i转换为double类型

在这个例子中,我们使用了(double)i进行了类型转换,将整数i转换为了double类型。

在C++中,类型转换更为复杂。以下是使用static_castconst_castdynamic_castreinterpret_cast的例子:

  1. static_cast
int i = 10;
double d = static_cast<double>(i);  // 将整数i转换为double类型

这个例子中,我们使用了static_cast<double>(i)进行了类型转换,将整数i转换为了double类型。

  1. const_cast
const int i = 10;
int* pi = const_cast<int*>(&i);  // 将const int指针转换为非const int指针

在这个例子中,我们使用了const_cast<int*>(&i)进行了类型转换,将const int指针转换为了非const int指针。

  1. dynamic_cast
class Base {
    virtual void foo() {}
};
class Derived : public Base {
    void foo() override {}
};

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b);  // 安全地将Base*转换为Derived*

在这个例子中,我们使用了dynamic_cast<Derived*>(b)进行了类型转换,安全地将Base*转换为了Derived*

  1. reinterpret_cast
int i = 10;
int* pi = &i;
char* pc = reinterpret_cast<char*>(pi);  // 将int指针转换为char指针

在这个例子中,我们使用了reinterpret_cast<char*>(pi)进行了类型转换,将int*转换为了char*

1.4 总结

  • 去掉const属性用const_cast
  • 基本类型转换用static_cast
  • 多态类之间的类型转换用dynamic_cast
  • 不同类型的指针类型转换用reinterpret_cast

2. C++什么时候生成默认构造函数

2.1 不会生成默认构造函数的情况

空的类定义,不会生成构造函数,没有意义,即便里面包含成员变量,成员变量时基础类型,还是不会生成默认构造函数,编译器也不知道用什么值去初始化

2.2 会生成默认构造函数的情况

  1. 类A内数据成员是对象B,而类B提供了默认构造函数
    • 为了让B的构造函数能被调用到,不得不为A生成默认构造函数
  2. 类的基类提供了默认构造函数
    • 子类构造函数要先初始化父类,再初始化自身成员变量
    • 如果父类没有提供默认的构造函数,子类也无需提供默认构造函数
    • 如果父类提供了默认构造函数,子类不得不生成默认构造函数
  3. 类内定义了虚函数
    • 为了实现多态机制,需要为类维护一个虚函数表
    • 类所有对象都需要保存一个指向该虚函数表的指针
    • 对象需要初始化该虚函数表的指针
    • 不得不提供默认构造函数来初始化虚函数表指针
  4. 类使用了虚继承
    • 虚基类表记录了类继承的所有的虚基类子对象在本类定义的对象内的偏移位置
    • 为了实现虚继承,对象在初始化阶段需要维护一个指向虚基类表的指针
    • 不得不提供默认构造函数来初始化虚基类表指针

3. C++什么时候生成默认拷贝构造函数

3.1 不显式指定拷贝构造函数

在C++中,编译器会为类生成一个默认的拷贝构造函数(也就是一个隐式的拷贝构造函数)的情况是,只要你没有为这个类明确地定义一个拷贝构造函数。这个默认的拷贝构造函数将执行每个成员的拷贝,这通常意味着执行成员的拷贝构造函数(对于类类型的成员),或者执行简单的位拷贝(对于内置类型的成员)。

这个行为是由 C++ 的标准所定义的。默认的拷贝构造函数通常是足够的,除非你的类需要管理自己的资源,比如动态分配的内存。在这种情况下,你需要定义你自己的拷贝构造函数,以正确地执行深拷贝,否则可能会出现问题(例如,浅拷贝导致的资源的多次删除)。

需要注意的是,如果你定义了任何其他的构造函数(比如移动构造函数或参数化的构造函数),但没有定义拷贝构造函数,编译器仍然会为你生成默认的拷贝构造函数。只有当你明确地定义了拷贝构造函数时,编译器才不会生成默认的拷贝构造函数。

3.2 必须生成拷贝构造函数的情况

同2.2

4. 浅拷贝和深拷贝

4.1 C++会出现拷贝的情况

  1. 初始化:当一个对象以另一个对象作为初始值时,会发生拷贝操作。例如,如果你声明了一个新的对象并以另一个对象作为初始值,那么会发生拷贝操作:

    MyClass obj1;
    MyClass obj2 = obj1;  // 拷贝操作
    
  2. 函数参数传递:当一个对象作为函数参数时,如果这个参数是按值传递的,那么在函数被调用时会发生拷贝操作:

    void foo(MyClass obj) { /* ... */ }
    
    MyClass obj;
    foo(obj);  // 调用 foo 时,obj 会被拷贝
    
  3. 函数返回:当一个函数返回对象时,如果这个对象是按值返回的,那么在函数返回时会发生拷贝操作:

    MyClass foo() {
        MyClass obj;
        return obj;  // 返回时,obj 会被拷贝
    }
    
  4. 赋值:当一个对象被赋予另一个对象的值时,会发生拷贝操作:

    MyClass obj1;
    MyClass obj2;
    obj2 = obj1;  // 拷贝操作
    
  5. 作为容器元素:当一个对象被插入到一个容器(例如,std::vectorstd::list等)时,会发生拷贝操作:

    std::vector<MyClass> vec;
    MyClass obj;
    vec.push_back(obj);  // obj 被拷贝到 vec 中
    

4.2 浅拷贝(Shallow Copy)

浅拷贝意味着仅复制对象的成员值,而不考虑这些值是否表示其他资源的引用。如果一个对象有指向动态分配内存或其他资源的指针,浅拷贝只会复制这个指针的值(即复制了内存地址),而不会复制该指针所指向的资源。

这可能会导致问题,因为当多个对象共享同一个资源时,一旦其中一个对象在析构时释放了这个资源,其他对象就会持有一个无效的指针。这称为悬挂指针(dangling pointer)问题。

以下是一个简单的浅拷贝的例子:

class ShallowCopyExample {
    int* data;
public:
    ShallowCopyExample(int value) {
        data = new int;
        *data = value;
    }

    // 浅拷贝复制构造函数
    ShallowCopyExample(const ShallowCopyExample& source) {
        data = source.data;
    }

    ~ShallowCopyExample() {
        delete data;
    }
};

4.3 深拷贝(Deep Copy)

相对的,深拷贝不仅复制对象的成员值,而且会为任何动态分配的资源创建新的副本。也就是说,如果一个对象有指向动态分配内存的指针,深拷贝会创建这段内存的一个新副本,并将新对象的指针指向这个新的内存。

这样做可以避免悬挂指针问题,因为每个对象都有自己的资源,不会和其他对象共享。

以下是一个简单的深拷贝的例子:

class DeepCopyExample {
    int* data;
public:
    DeepCopyExample(int value) {
        data = new int;
        *data = value;
    }

    // 深拷贝复制构造函数
    DeepCopyExample(const DeepCopyExample& source) {
        data = new int;  // 为新对象分配内存
        *data = *source.data;  // 复制资源
    }

    ~DeepCopyExample() {
        delete data;  // 释放对象的资源
    }
};

需要注意的是,C++默认的复制构造函数和赋值运算符都是执行浅拷贝的。如果你的类管理动态分配的资源,你需要重写自己的复制构造函数和赋值运算符,以实现深拷贝

5. const关键字的作用

5.1 定义变量

  1. 局部的const

    局部const是指在函数内部声明的const变量。这种变量只在声明它的函数体内部可见,且在其作用域内其值不能被改变。

    例如:

    void func() {
        const int x = 10;  // x是局部const
    }
    
  2. 全局的const

    全局const是指在函数外部声明的const变量。这种变量在整个程序中都是可见的,且其值不能被改变。

    例如:

    const int y = 20;  // y是全局const
    
    void func() {
        // 可以在这里使用y
    }
    
  3. 符号表

    符号表是编译器用来存储程序中使用的所有变量、函数等标识符的信息。对于const变量来说,它们的值和类型都会被存储在符号表中。有如下的作用:

    1. 类型检查:编译器通过查阅符号表中的类型信息来进行类型检查。如果一个 const 变量被用于一个需要不同类型的表达式中,编译器可以发出警告或错误。

    2. 值替换和优化:由于 const 变量的值在编译时就已知,所以编译器可以在编译和优化过程中直接使用这些值。例如,如果你有一个表达式 const int x = 5; int y = x * 2;,编译器可能直接将其优化为 int y = 10;。这可以提高运行时的性能。

    3. 保护 const 变量的值:由于 const 变量的值在符号表中,编译器可以确保在程序中的任何地方都不会改变这个值。如果试图修改一个 const 变量的值,编译器会发出错误。

  4. 常量指针

    常量指针是指向const对象的指针,也就是说,你不能通过这种指针修改它所指向的值,但是你可以改变这个指针所指向的地址。

    例如:

    const int x = 10;
    const int* p = &x;
    

    在这个例子中,p是一个指向const int的指针,所以你不能通过p来改变x的值。

  5. 指针常量

    指针常量是一种你不能改变其所指向的地址的指针,但是你可以通过这种指针修改它所指向的值。

    例如:

    int x = 10;
    int* const p = &x;
    

    在这个例子中,p是一个const指针,所以你不能改变p的值(也就是说,你不能让p指向其他的地址),但是你可以通过p来改变x的值。
    6.作用

    • 避免修改
    • 避免多次内存分配
    • 类型检查、作用域检查

5.2 修饰函数参数

在 C++ 中,const关键字修饰函数参数有以下几个主要作用:

  1. 防止参数值被修改:当你使用const关键字修饰一个函数参数时,你就不能在函数体内修改该参数的值。这可以防止函数中的错误修改。

    void foo(const int x) {
        x = 42;  // 编译错误,因为x是const
    }
    
  2. 实现接口承诺:const参数可以向使用函数的程序员传达重要信息。它明确表明函数不会修改参数的值。这是一种很好的自我文档,并能帮助防止使用函数的人产生错误。

  3. 提高函数的通用性:const引用或指针参数允许函数接受const和非const实参,而非const引用或指针只能接受非const实参。这使得函数可以在更多的上下文中被使用。

    void bar(const std::string& str) {
        // str 是 const 引用,不能在函数体内被修改
    }
    
    const std::string str1 = "Hello";
    std::string str2 = "World";
    
    bar(str1);  // 有效,因为str1是const
    bar(str2);  // 也有效,因为非const实参可以转换为const引用
    
  4. 提高性能:对于大对象,通过const引用传递参数可以避免复制操作,同时保证函数不会修改参数。

    需要注意的是,对于基本数据类型(如 intfloat 等),以值传递参数并使用 const 修饰通常并不会带来实质性的性能优势,因为复制这些类型的代价很小。然而,对于大型类和结构,通过 const 引用传递参数可以带来显著的性能优势。

5.3 修饰函数返回值

在 C++ 中,const关键字修饰函数返回值主要有以下几个作用:

  1. 返回对象的只读版本:如果函数返回一个对象的引用或指针,并且你不希望调用者通过这个引用或指针修改这个对象,你可以使用const关键字来修饰返回类型。这意味着函数返回的引用或指针是只读的,不能用来修改对象。

    const int& foo() {
        static int x = 0;
        return x;
    }
    

    在这个例子中,foo函数返回一个对intconst引用,这意味着你不能通过这个引用来修改x

  2. 防止意外的对象修改:const关键字能防止你在无意中修改了不应该修改的对象。这是一种安全机制,能够帮助你避免一些可能导致错误的行为。

  3. 增强接口语义:在某些情况下,const关键字能够提供更清晰的语义。例如,如果你有一个成员函数,这个函数返回一个成员变量的引用,并且这个函数不应该改变对象的状态,你可能会选择返回一个const引用。

需要注意的是,对于返回一个局部对象的函数,返回类型通常不应该是const引用或指针,因为当函数返回后,局部对象就不存在了。在这种情况下,你应该返回一个值或者一个指向动态分配内存的指针。

5.4 类中的常成员函数

  1. 注意点

    • 不能在常成员函数中修改类的任何非静态成员变量
    • 只读对象只能调用常成员函数
    • 不能在常成员函数中调用类中的任何非常成员函数和非常成员变量,因为非常成员函数可能会修改类的成员变量,非常成员变量也会被修改
  2. 作用

    • 避免成员变量被无意修改,更加的安全
    • 可以用于函数重载,常成员函数与非常成员函数是不一样的两个函数,这样根据对象是否是常量来调用不同版本的函数
    • 允许常对象调用其成员函数,如果有一个需要在常对象上调用的成员函数,那么这个成员函数应该是常成员函数

6. 补充

  1. 野指针和悬挂指针的区别
    野指针(Wild Pointer)通常是指任何未初始化或者未正确设置的指针,而悬挂指针(Dangling Pointer)通常是指指向已经释放或者超出生命周期的对象的指针。

  2. 运算符重载
    在 C++ 中,运算符重载是一种使你能够改变某种类型(特别是用户定义的类型)的运算符行为的语言特性。你可以通过定义特殊的成员函数或全局函数来重载运算符。

    以下是一个重载 + 运算符的例子。我们定义了一个 Complex 类,它表示复数,然后重载 + 运算符来实现复数的加法。

    class Complex {
    public:
        Complex(double real, double imag) : real_(real), imag_(imag) {}
        double real() const { return real_; }
        double imag() const { return imag_; }
        Complex operator+(const Complex& other) const {
            return Complex(real_ + other.real_, imag_ + other.imag_);
        }
    
    private:
        double real_;
        double imag_;
    };
    
    int main() {
        Complex a(1.0, 2.0);
        Complex b(3.0, 4.0);
        Complex c = a + b;  // 使用我们重载的 + 运算符
        // c.real() 的值为 4.0,c.imag() 的值为 6.0
        return 0;
    }
    

    注意以下几点:

    • 运算符重载不会改变运算符的优先级。例如,即使你重载了 +* 运算符,* 运算符仍然比 + 运算符有更高的优先级。
    • 并非所有的运算符都可以重载。例如,.(成员访问)运算符、::(作用域解析)运算符、sizeof 运算符和 ?:(条件)运算符不能被重载。
    • 你不能改变运算符的 “基本语义”。例如,你不能定义一个使 && 运算符执行乘法的重载。
    • 你不能创建新的运算符。你只能重载已存在的运算符。
    • 对于二元运算符(如 +-),你可以选择将其重载为成员函数或全局函数。如果你选择将其重载为成员函数,那么第一个操作数就是调用该函数的对象。如果你选择将其重载为全局函数,那么你需要为两个操作数都提供参数。
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。