侯捷课程笔记(一)(传统c++语法,类内容)

侯捷课程笔记(一)(传统c++语法,类内容)

2023-09-03更新:

本小节已经完结,只会进行小修改

埋下了一些坑,后面会单独讲或者起新章节讲

最近在学习侯捷的一些课程,虽然其中大部分内容之前也都已经了解过了,不过还是收获颇丰,特别是独立与所谓语法之外的,还有许多与设计相关的。

这一章内容比较简单,也就直接摆出内容吧, 相关重点内容就提一提,大部分都直接掠过了。然后会加入一些自己的侯捷没有讲到的内容。

那就直接开始吧


类设计时头文件防止重复包含

通常用法就是

cpp 复制代码
#ifndef FOO_H__
#define FOO_H__
// ... 主体内容

#endif // FOO_H__

因为这个很常用,但是每次都要写三行,也就有了简化版本

cpp 复制代码
#pragma once

不过这个可能需要编译器支持(大部分肯定都是没问题的)


类内成员函数默认就是inline

inline就是内联,inline允许我们在头文件中定义函数重复包含时而不会发生重复定义问题

由于很多时候我们写类是在头文件中(声明在头文件),这时候,如果成员函数定义在类内,不也就是把函数放在了头文件吗,编译器比较智能,也就默认给类内定义的成员函数自动加上了inline属性

如果我们要把类的成员函数写在类外,就没有inline这个属性了,这时候如果还在头文件,我们就要手动加上inline


类使用初始化列表对成员进行初始化

初始化列表其实也就只有两个需要注意的点:

  1. 初始化的顺序不是按照写的顺序来的,而是按照成员变量定义的顺序来的
  2. 如果父类没有默认构造函数,也就是子类必须显式调用函数初始化父类,这时候也就必须要使用初始化列表初始化父类

类内对函数加上const(常函数)

正常的成员函数是可以更改类的,所以对于一个const属性的类,编译器考虑到这个类不可以更改,也就不允许它调用普通的成员函数,只能调用常函数(带const属性的函数)

一个良好的变成习惯是:对于一些不会更改成员变量/类属性的函数,都应该加上const属性,比如获取变量GetVal类似的函数

ps:使用const实例化一个对象,这个类一定要有用户定义的构造函数


对于第一点,比如下面的代码:

cpp 复制代码
struct A {
  int a;
  int b;
  A() : b(1), a(2) {}
};

在这里初始化的顺序实际上是先执行a=2,再执行b=1,因为a是先定义的那个

看起来好像无所谓,但是下面的代码

cpp 复制代码
struct A {
  int a;
  int b;
  A() : b(1), a(b) {}
};

就会出现a先初始化,但是这时候b还没有初始化的问题


对于第二点,比如

cpp 复制代码
struct Base {
  int a;
  Base(int){}
};
struct Derive : public Base {
  Derive(){};
};

父类没有默认的构造函数,子类必须要也只能通过初始化列表初始化父类

cpp 复制代码
struct Derive : public Base {
  Derive():Base(10){};
};

对于一个类来说,常用的构造函数或者基本架构是什么样的

在侯捷的课程中,常写的是

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值函数

其中如果类中包含指针,常常会自己写拷贝构造和拷贝复制,并会在析构函数中对指针进行处理

而在c++11后引入右值的概念

增加了

  • 移动构造函数
  • 移动复制函数
cpp 复制代码
class MyClass {
 public:
  MyClass();
  MyClass(const MyClass &) = default;
  MyClass(MyClass &&) = default;
  MyClass &operator=(const MyClass &) = default;
  MyClass &operator=(MyClass &&) = default;
  ~MyClass();
};

类中使用友元和重载cout

我们通常会这么写,并且也可以直接把重载函数写在类内

cpp 复制代码
struct A {
 public:
  friend std::ostream& operator<<(std::ostream& os,const A& obj) {
    os << obj.a;
    return os;
  }
 private:
  int a;
};

可以注意几点:

  • 加上friend就可以访问类中的私有变量
  • 输入的os没有加上const,是因为os执行<<会更改内部内容,无法使用const
  • 输入的引用是为了避免不必要的拷贝,obj加上const是加上&后也可以传入右值
  • 返回引用是可以使用链式编程

带有指针的类的结

如果类内带有指针,比如类的构造函数里会new一块内存。

  • 需要在析构函数内释放对应的内存
  • 需要注意深拷贝和浅拷贝的问题
    • 拷贝构造函数和复制构造函数需要重写,重新开辟内存来进行深拷贝
  • 在赋值构造前需要注意是否是自身赋值(自己赋值给自己),需要判断这种特殊情况以防止bug

比如下面这个示例,注意关注一下前面提到的几点

cpp 复制代码
template <typename T>
struct A {
 public:
  A(int n) {
    size = n;
    str = new T[n];
  }
  ~A() { delete[] str; }
  A(const A& obj) { Copy(obj); }
  A& operator=(const A& obj) {
    if (&obj == this) return *this;
    Copy(obj);
    return *this;
  }
  void Copy(const A& obj) {
    if (str) delete[] str;
    size = obj.GetSize();
    str = new T[size];
    std::memcpy(str, obj.str, size * sizeof(T));
  }
  int GetSize() const { return size; }

 private:
  int size = 0;
  T* str = nullptr;
};

内存分配和管理

这一部分其实单独属于一块内容,后面会单独讲


设计模式(单例模式或者其它)

这一部分其实单独属于一块内容,后面会单独讲


类转换成标准类型(operator int())

类可以使用operator转换成一些类型,在编辑器认为转化可以通过编译时会进行转换,当然我们也可以使用static_cast显式转化

cpp 复制代码
struct A {
 public:
  A(int a_) { a = a_; }
  int a;
  operator int() {
    return a;
  }
  operator float() {
    return a;
  }
}
void Test() {
 Aa(10);
 int b=10+static_cast<int>(a);
 int c=static_cast<float>(a)+100;
}

需要注意的是,这个operator函数并不需要返回值,默认返回值类型就是你写的要转换的类型

这里operator不光可以转为内置类型int/float,还可以转换成自己写的类等等

类通过单参数的构造函数自动转化/explicit

c++可以通过operator将类转化成一些类型,同样也支持反向转化,编译器可以自动根据单参数输入的构造函数,将对应的参数的自动执行构造函数构造对象

💡:这里说的单参数,只指可以输入是一个参数的函数,比如一个函数有三个三数,但是后面两个带了默认参数,也满足单参数;又或者只有一个参数,并且这个参数是默认参数,也属于单参数

cpp 复制代码
struct A {
 public:
  A(int a_) { a = a_;}
  int a;
  A operator+(const A& obj) {
    return A(a+obj.a);
  }
};

比如我要执行: A a(10); A b=a+10; 这里重载了+运算符,c++会自动将后面的10调用构造函数,转换成一个类

如果是A b=10+a就不行

这里的+运算符属于内置的int,在这里就不会默认转换

如果要运行这句话,就要用前面的operator int()

cpp 复制代码
operator int() {
  return a;
}

运行的顺序就是先把a转换成int,然后10+整数,然后将加之后的结果转换成A类类型赋值给b

如果我们同时写了单参数的构造函数和operator 类型(),就有可能出现冲突,就比如上面的例子:

cpp 复制代码
struct A {
 public:
  A(int a_) { a = a_;}
  int a;
  A operator+(const A& obj) {
    return A(a+obj.a);
  }
  operator int() {
    return a;
  }
}

void Test() {
 A a(10);
 A b=a+10;
 A c=10+a;
}

这里的A b=a+10;就会有问题,因为冲突了

  • 因为既可以把10调用构造函数转换成类然后执行operator +
  • 也可以把a转换成int,然后加了后再调用构造函数转换成类

这时候我们就可以使用explicit显式的禁止允许单参数的构造函数的默认转换,这样那个构造函数就只允许我们显式调用,而不允许转换了。在示例中也就是不允许10转换成类的类型了


让类表现出指针形式(重载*->

我们可以重载*->让类表现出类似于指针的形式,使用示例比如说智能指针和容器的迭代器

cpp 复制代码
struct A {
 public:
  int& operator*() const {
    return *p;
  }
  int* operator->() const {
    return &(this->operator*());
    // return p;
  }
  int* p;
};

在这里,*用来表现解引用,->用来表现指针的成员内容,一般来说->在函数返回值表现形式后还会有一个潜在的->(设计如此)


让类表现出函数形式(重载括号运算符)

这个其实非常常用,比如在ceres库中用于传递代价函数,平时也把这种函数叫做仿函数(模仿函数?)

cpp 复制代码
struct A {
 public:
  template <typename T>
  T operator()(const T& a,const T& b) {
    return a>b?a:b;
  }
};
void Test() {
  A a;
  std::cout << a(10,20);
}

函数模板/类模板/模板模板参数

对于模板来说,关键字classtypename是一样的 ,为啥有两个?历史原因,不重要了
函数模板:

cpp 复制代码
template <typename T>
void Foo(T t) {}

类模板:

cpp 复制代码
template <typename T>
struct Foo {
  Foo(T t) {}
}

模板模板参数:

cpp 复制代码
template <typename T>
struct Foo {
  T foo;
};

template <typename T,template <typename> class my_class>
struct A {
  my_class<T> class_a;
};

template <typename T1,typename T2>
struct Goo {
  T1 goo1;
  T1 goo2;
};
template <typename T1,typename T2,template <typename,typename> class my_class>
struct B {
  my_class<T1,T2> class_b;
};

void Test() {
  A<int,Foo> a;
  B<int,float,Goo> b;
}

其中:template <typename> class my_class是一个示例,表示输入的是带有一个模板参数的类

  • template表示这是一个模板
  • typename的个数表示对应类的模板个数,需要和传入的模板类对应
  • class 也是个关键词,和typename一样,换成typename也是可以的
  • my_class是这个模板名

在上面的示例中,分别给了一个参数和两个参数的模板类传参示例


模板特化和偏特化

其实特化也就是指定模板的某一个或者任意个参数。

所以特化也就分成全特化和偏特化,全特化就是所有模板参数都指定的特化,偏特化就是只指定部分的特化

有一条规则是函数模板只允许全特化,不允许偏特化,类模板允许偏特化

比如在c++标准库中判断一个参数是否为整数的源码:

cpp 复制代码
// Integer types
template <typename _Tp>
struct __is_integer {
  enum { __value = 0 };
};
template <>
struct __is_integer<bool> {
  enum { __value = 1 };
};
template <>
struct __is_integer<char> {
  enum { __value = 1 };
};
template <>
struct __is_integer<signed char> {
  enum { __value = 1 };
};
template <>
struct __is_integer<unsigned char> {
  enum { __value = 1 };
};
...
后面有其它整数类型包括了int和long,都是和前面一样的
...

在这里列出所有整数类型,只要是整数就会进入到特化的版本,这样只需要根据__value的值就可以判断了

上面这种形式就是全特化

再给个函数模板全特化的例子:

cpp 复制代码
template <typename T1,typename T2>
void Foo(T1 a,T2 b) {
  std::cout << "Test1" << std::endl;
}

template<>
void Foo<int,float>(int a,float b) {
  std::cout << "Test2" << std::endl;
}

void Test() {
  Foo<int,int>(10,20);
  Foo<int,float>(10,20);
  Foo(10,20);
  Foo(10,20.0f);
}

下面演示下类模板偏特化和全特化

cpp 复制代码
template <typename T1,typename T2>
struct A {
  A(T1 a,T2 b) {std::cout << "A" << std::endl;}
};

template <typename T>
struct A<int,T> {
  A(int a,T b) {std::cout << "A<int,T>" << std::endl;}
};

template <typename T>
struct A<float,T> {
  A(int a,T b) {std::cout << "A<float,T>" << std::endl;}
};

template <>
struct A<int,int> {
  A(int a,int b) {std::cout << "A<int,int>" << std::endl;}
};

template <>
struct A<int,float> {
  A(int a,float b) {std::cout << "A<int,float>" << std::endl;}
};

void Test() {
  A("10","20");
  A(10,"20");
  A(10.0f,"20");
  A(10,20);
  A(10,20.0f);
}

在上面的例子中就是给了两个偏特化和两个全特化

通过前面的全特化都可以看出来,全特化一般会伴随template <>出现(因为全都指定了,也就没有T类型了)


auto/右值引用/变长模板 等c++11的内容

这些内容后面都会单独讲,而且其中很多内容在c++14/c++17会有变化和增强

比如右值在c++17后定义更加成熟,变长模板增加了一些展开方式

后面单独讲比在这里粗浅的讲要好


继承/虚函数/虚表

这一块最好也是单独开一块内容来讲,关于虚的内容还是很多的。底层实现也很值得研究。


相关推荐
Yawesh_best6 分钟前
告别系统壁垒!WSL+cpolar 让跨平台开发效率翻倍
运维·服务器·数据库·笔记·web安全
爱学习的小邓同学32 分钟前
C++ --- 多态
开发语言·c++
Ccjf酷儿2 小时前
操作系统 蒋炎岩 3.硬件视角的操作系统
笔记
习习.y3 小时前
python笔记梳理以及一些题目整理
开发语言·笔记·python
在逃热干面3 小时前
(笔记)自定义 systemd 服务
笔记
DKPT5 小时前
ZGC和G1收集器相比哪个更好?
java·jvm·笔记·学习·spring
QT 小鲜肉6 小时前
【孙子兵法之上篇】001. 孙子兵法·计篇
笔记·读书·孙子兵法
招摇的一半月亮7 小时前
P2242 公路维修问题
数据结构·c++·算法
星轨初途7 小时前
数据结构排序算法详解(5)——非比较函数:计数排序(鸽巢原理)及排序算法复杂度和稳定性分析
c语言·开发语言·数据结构·经验分享·笔记·算法·排序算法
QT 小鲜肉7 小时前
【孙子兵法之上篇】001. 孙子兵法·计篇深度解析与现代应用
笔记·读书·孙子兵法