C++ 踩坑日记

Tue Nov 28 2023

CPP 踩坑日记

本来我是不爱记笔记的,但是没办法啊,这cpp东西有亿点多,而且和之前写的那些语言(C# / Python / TypeScript)完全不能用一个思路去思考,所以感觉还是得稍微记点东西(作业做到什么就记什么

以及你可能会在本篇中看到一堆,把其他语言的特性拉出来与cpp对比的场景(因为咱真的不会cpp喵呜呜呜

## const 修饰符

cppreference里面把const和volatile放在一起,合称cv (const and volatile) type qualifiers,这个const用起来是真的有点难评

  const string toString() const;

比如,在上面这一行中,就出现了两个const,第一个const用来修饰string,也就是返回类型,表明返回的值是一个不可变的string,第二个const则修饰函数主体

对返回类型的修饰其实还好理解,一个const SomeType A就表明这个A是只读的,类似于C#的readonlyreadonly field的用法,即初始化初始化后不可变。但不同之处在于C#的readonly一般只能用在class和struct里面作为field的修饰符,在声明时初始化,或者在构造函数/静态构造函数时初始化,cpp的这个const则相对灵活得多,可以在几乎任何地方使用,比如方法内部声明一个const的变量(笑死,什么不可变的变量)。而且C#的readonly更多的是对一个字段的修饰,cpp的const则更多是对一个类型的修饰。

而第二个const则是对方法主体的一个限制,标记这个方法不可以修改this的所指向的对象的字段,也就是只能读取自身的字段的值,而不能修改,CppReference也给这种对于成员方法的cv修饰符进行了解释。(之前都没看到,今天翻C#的文档的时候发现,什么时候又加了这个语法,这下看懂了

现在假设有这样一个字段的声明:

const SomeType a = new SomeType();

SomeType定义了两个public的方法:

const string toString() const;
const string toString2();

那么对于对象实例a,只能访问a.toString()而不能访问a.toString2(),因为toString方法声明了自己不对对象进行修改,而toString2没有,因此,如果调用toString2,编译器会直接报错,即使toString2本身在实现的时候并没有修改对象本身。

update 1: cpp中的两个同名函数,根据是否被标记为const,可以分成两个方法重载(overload),如以下代码,实际编译器并不会报错,因为可以视为两个方法的signature不同

const string toString() const;
const string toString(); // no error raised

贴一段cppreference上面的example

int main()
{
    int n1 = 0;          // non-const object
    const int n2 = 0;    // const object
    int const n3 = 0;    // const object (same as n2)
 
    const struct
    {
        int n1;
        mutable int n2;
    } x = {0, 0};        // const object with mutable member
 
    n1 = 1;   // OK: modifiable object
//  n2 = 2;   // error: non-modifiable object
//  x.n1 = 4; // error: member of a const object is const
    x.n2 = 4; // OK: mutable member of a const object isn't const
 
    const int& r1 = n1; // reference to const bound to non-const object
//  r1 = 2; // error: attempt to modify through reference to const
    const_cast<int&>(r1) = 2; // OK: modifies non-const object n1
 
    const int& r2 = n2; // reference to const bound to const object
//  r2 = 2; // error: attempt to modify through reference to const
//  const_cast<int&>(r2) = 2; // undefined behavior: attempt to modify const object n2

const没记错的话应该还有别的用法,下次再补充,摸了

## 赋值运算符(operator=)重载(overloading)

第一次看到赋值运算符,即operator=居然是可以重载的时候,感觉有亿点点不可思议,但仔细了解之后可以发现,这其实是因为与我常用的其他语言的一些差异导致的。

在cpp中,this是一个特殊的指针,是一个指向当前对象的指针。但其本质还是指针,所指向的还是一个内存上的地址,因此在调用赋值运算符operator=的时候,其实应该理解为对this所指向内存区域的一个操作,因此存在operator=的重载并不奇怪,这个operator=operand应该为this所指向的内存区域和一个新的对象实例(通过值或者引用传递)。

但其他语言,比如C#和java中,为什么没有复制运算符的重载呢?我的理解是,C#和java的this并不是一个指针,而是一种引用,这个引用强绑定到当前对象实例。因此在对变量赋值的时候,其实是和上一个对象实例无关的操作,无法定义一个赋值运算符的重载,因为没办法确定运算符的operand,在C#和java这种managed的语言中,相关的内存管理操作全部已经由gc接管了。

## 指针(pointer)与引用(reference)与智能指针(smart pointer)

这也要分这么细来恶心人是吧算了,好像区别还是挺大的(

### Pointer

### Reference

### Smart Pointer

  1. Microsoft Docs

## 模板 template

template其实就是cpp对泛型的一种实现,虽然看起来和别的语言的泛型区别有亿点点大。

cpp的template是一种metaprogramming,其实是编译器自动在编译期实现了各个不同参数的实现

template<arg1, arg2, ...args>
  1. Microsoft Docs
  2. CppReference: Template parameters and template arguments
  3. CppReference: Template
  4. CppReference: Dependent names

### error ld: undefined reference

参考

  1. StackOverflow: Undefined reference error for template method
  2. StackOverflow: Why can templates only be implemented in the header file?
  3. StackOverflow: Error "Undefined reference to operator<<" in C++ code

## friend keyword

  1. CppReference

## 继承

### 公有继承、保护继承、自由继承

捏猫猫的,这cpp怎么这么多事

假如一个DerivedClass继承自一个BaseClass,公有继承public、保护继承protected、私有继承private可以简单理解为:

原来通过DerivedClass的实例derivedInstance访问BaseClass中的member可以看成derivedInstance.super.member(虽然实际用起来不是,但是可以这么理解,而且本质上的实现就是在派生类里面放一个基类对象)

三种继承方式的区别就相当于给derivedInstance中的这个“super”加上一个访问修饰符。

对于继承自DerivedClass的派生类同理。

### cpp的多重继承

多重继承就是一个class可以同时继承自多个class因为cpp没有interface。多重继承会导致一个问题:class是有自己的field的,是要占用内存空间的,这就导致在多重继承是,假如多个父类存在一个共同的父类,就会导致编译器创建了多块独立的内存来存储这个共同父类的字段,且如果在子类调用冲突的父类字段,会导致编译器报错。

问题来了,为什么java和C#中没这个问题能?因为有interfaceinterface只约定了对象方法,不涉及对象的字段和内存,而且是单继承,不允许同时继承自多个父类。

### 虚继承

在继承时加上virtual即为虚继承。虚继承不会创建重复的父类,也就不存在字段冲突的问题。

class A{ 
protected:
    int m_a;
};
 
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
 
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
 
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

## 浅层复制和深层复制