跟着侯捷老师学C++☛基础概念走一走

249 阅读12分钟

作为一个初学者,看侯捷老师的视频学习C++
顺手记录一把自己的学习点,可能很基础

namespace

空间命名,区分重名

using namespace std;

cin << ...;
cout << ...;
using namespace std::count;

std::cin << ...;
cout << ...;
std::cin << ...;
std::cout << ...;

头文件相关

引入

通过头文件 .h 来引入库

#include <iostream.h>  // 引入标准库
#include <cstdio>  // 引入标准库,可以不带.h
#inlcude "complex.h"  // 自定义库

定义头文件标准注释

complex.h 头文件,标准格式如下

#ifndef __MYCOMPLEX__
#define __MYCOMPLEX__

...

#endif

方法声明方式

  1. 前置声明
class complex; 
complex&
  __doapl (complex* ths, const complex& r);

2. 类声明

class complex
{
  ...
};

3. 类定义

complex::function ...

函数

内联函数

函数在class内定义就是内联函数,如类声明函数。 若是在class外部定义,也可以添加 inline 关键字使其成为内联函数。

class complex
{
  ...
};

inline double imag(){
  ...
}

然而,是否成为内联函数,是由编译器去决定的。我们添加inline关键字,只是对编译器的建议,让编译器尽量把这些函数内联。包括class内声明的函数也是这样

析构函数

~ClassName表示析构函数。在对象即将销毁时执行。 在含有指针的类里,一定要有析构函数。

static

class Account {
public:
  static double m_rate;
  static void set_rate(const double& x) { m_rate = x; }
};
  double Account::m_rate = 8.0;
int main() {
  Account::set_rate(5.0);
  Account a;
  a.set_rate(7.0);
}

static属性声明与定义

声明成static:static double m_rate;

定义,可以不赋初值:double Account::m_rate = 8.0;

static函数调用方式

静态函数里面没有隐性的this指针,所以只能处理静态数据

  1. 通过object调用
  2. 通过class name调用

构造与析构的default

class A : public B {

public:
    A() = default;
    ~A() override = default;
}

上述代码中的 A() = default; 这个default是什么意思?

使用= default可以指定使用默认实现。它告诉编译器使用默认生成的构造函数或析构函数,而不需要显式定义它们。默认构造函数没有任何参数,并进行默认的对象初始化操作。默认析构函数负责在对象销毁时执行必要的内存清理和资源释放操作。

常量成员函数

class complex
{
public:
  double real () const { return re; }
  double imag () const { return im; }

上面的函数后面有const,这个位置放const一般只在成员函数中这么用,不会在全局函数中使用。这表示这个函数不会改变这个class内的数据。

non-const member function:不保证data members不变 const member function:保证data members不变 const object:data members 不能改变 non-const object:data members 可以改变

所以const object不能调用non-const member function。

const String str("hello");
str.print();

// 这里可以看到,str是一个const objetct,表示我这个字符串不能改变
// 如果 string::pringt()在设计时,如果未指明这是const member function,这时候就会编译报错

如果确定一个member function不会改变data,那在设计时就加上const!避免后续使用不通过

当成员函数的const和non-const版本同时存在时,const object只能调用const版本。non-const object只能调用non-const函数版本

这里的const会影响函数签名

重载

函数重载

同名函数(包括构造函数)可以有很多个,只要最终签名不一样

class complex
{
public:
  double real () { return re; }
}

inline double
real (const complex& x)
{
  return x.real ();
}

上述在一个文件里定义了两个real,但由于入参不一样,所以real函数编译后的实际名称可能是:

?real@Complex@@QBENXZ
?real@Complex@@QBENABN@Z
  • 函数返回指不影响签名
  • 函数是否const影响签名,如 double real () { return re; }double real () const { return re; }

构造函数重载

class complex
{
public:
  complex (double r = 0, double i = 0): re (r), im (i) { }  // 构造1
  complex () : re(0), im(0) {}  // 构造2
}

上述代码是会报错的。 因为构造1中,虽然接收入参,但入参没有的时候可以默认赋值为0。 在构造2中,不接收入参。

看起来两个签名是不一样的。但在实际调用时,complex c1(),对编译器来说,两个构造函数都满足条件,编译器无法选择,所以这样写是有问题的

操作符重载

操作符作用在左边的变量上。 例如 C2 += C1,编译器会去找C2上的 += 操作符

inline complex&
complex::operator += (const complex& r)
{
  return __doapl (this, r); // 任何成员函数都有个默认的this指针
}

:: 和 . 和 .* 和 ?: 这四个操作符不支持重载

const 修饰

const修饰指针有三种情况

  • const修饰指针 --- 常量指针
  • const修饰常量 --- 指针常量
  • const即修饰指针,又修饰常量 示例:
#include <iostream>
using namespace std;

int main()
{

	// 1、const 修饰指针  常量指针
	int a = 10;
	int b = 20;

	const int* p = &a;
	// 指针指向的值不可以该,指针的指向可以该
	// *p = 20; 错误
	p = &b; // 正确

	// 2、const 修饰常量 指针常量
	// 指针指向不可以改,指针指向的值可以改
	int* const p2 = &a;
	*p2 = 100; // 正确的
	// p2 = &b; // 错误,指针的指向不可以改

	// 3、const 修饰指针和常量
	const int* const p3 = &a;
	// 指针的指向 和 指针指向的值 都不可以改
	// *p3 = 100; // 错误
	// p3 = &b; // 错误

	system("pause");

	return 0;
}

生命期

栈stack

存在于某个作用域的一块内存空间。例如调用函数,函数本身会行程一个stack来存放接收的参数以及返回地址。

在函数本体内声明的任何变量,其所使用的内存块都取自上述stack

堆heap

或者成为system heap,由操作系统提供的一块global内存空间。程序可动态分配来获取这块空间

Complex c1(1,2);  // c1所占空间来自stack
Complex* p = new Complex(3);  // Complex(3)是个临时对象,其占用的内存是由new方法动态分配的heap,然后由p来指向这块内存
  1. stack objects 的生命期
class Complex { ... };
...
{
  Complex c1(1,2);
}

c1 便是stack object,其生命期在作用域结束之际结束。这种作用域内的object又称auto object,会被自动清理掉。

  1. static local objects 的生命期
class Complex { … };
...
{
  static Complex c2(1,2);
}

c2 便是static local objects,其生命期在作用域结束之后仍然存在,直到整个程序结束

  1. global objects 的生命期
class Complex { … };
...
Complex c3(1,2);
int main()
{
...
}

c3 便是global objects,写在函数外,也就是{}外的。其生命期在整个程序结束之后才结束,可以看成是static object。

  1. heap objects 的生命期
class Complex { … };
...
{
Complex* p = new Complex;
...
delete p;
}

P 所指的便是heap object,其生命在它被deleted 之后結束。如果没有delete,会产生内存泄漏,因为当作用域结束了,p所指的heap object仍然存在,但p本身已经结束了,作用域外再也看不到p,也没办法delete p了

模板

class template

格式:template<typename T>

template<typename T>
class complex
{
public:
  complex (T r = 0, T i = 0)
    : re (r), im (i)
  { }
  complex& operator += (const complex&);
  T real () const { return re; }
  T imag () const { return im; }
private:
  T re, im;
  friend complex& __doapl (complex*, const complex&);
};
// 使用
{
  complex<double> c1(2.5,1.5);
  complex<int> c2(2,6);
  ...
}

function template

格式:template <class T>

template <class T>
inline
const T& min(const T& a, const T& b)
{
  return b < a ? b : a;
}
// 使用
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);

这里对要比较大小的object没有要求,只要object有<方法就行。所以对于上述代码,只要在stone类里有操作符重载即可,不用<>符号来固定类型,因为编译器会对function template 进行引数推导(argument deduction)

class stone
{ 
public:
  stone(int w, int h, int we)
    : _w(w), _h(h), _weight(we)
      { }
  bool operator< (const stone& rhs) const
    { return _weight < rhs._weight; }
private:
  int _w, _h, _weight;
};

member template

struct pair{
  typedef T1 first_type;
  typedef T2 second_type;
  
  T1 first;
  T2 second;
  
  pair()
    : first(T1()), second(T2()) {}
  pair(const T1& a, const T2& b)
    : first(a), second(b) {}
	
  // 下面就是member template
  // 下面本身是模板中的一个member,但它本身又是template
  template <class U1, class U2>
  pair(const pair<U1,U2>& p)
    : first(p.first), second(p.second) {}
}

pair本身是一个template,有T1 T2变化。其内部变量又可以变化U1 U2。

//使用到这个member template
pair<Base1,Base2>p2(pair<Derived1,Derived2>());

image.png 上面的Derived1就是U1,Derived2就是U2 Base1就是T1,Base2就是T2。

最终效果就是将Derived1赋值给first,Derived2赋值给second

就是我限制了我这个pair必须由Base1和Base2构成。但由于里面的member template的存在,所以Base1和Base2的子类来赋值也没问题

模板偏特化

泛化的时候,某一些类型需要走其他操作,就可以再定义一个特化

个数的偏

// 本来有两个模板参数,第二个有默认值
template<typename T,typename Alloc=...>
class vector{
  ...
};

如果我已知vector的第一个参数类型,那么可以使用偏特化

template<typename Alloc=...>
class vector<bool, Alloc>{
  ...
};

范围的偏

template <typename T>
class C
{
  ...
}

上面是正常的模板,可以接受任意类型。
下面是偏特化,只能接受指针类型

template <typename T>
class C<T*>
{
  ...
}

使用如下:

C<string> obj1;
C<string*> obj2; // 会走下面的偏特化

设计模式

组合Composition

has-a关系

template <class T, class Sequence = deque<T> >
class queue {
  ...
protected:
  Sequence c; // 底层容器
public:
  // 以下完全利用 c 的操作完成
  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  reference front() { return c.front(); }

queue里面有deuqe(可以有多个) image.png

image.png

  • 构造函数由内而外(先component)
  • 析构函数由外向内(先container)

component和container的生命周期是一起的,是同步的

委托Delegation/composition by reference

class StringRep;
class String {
public:
  String();
  String(const char* s);
  String(const String& s);
  String &operator=(const String& s);
  ~String();
  . . . .
private:
  StringRep* rep; // pimpl
};

String里面有一根指针,这个指针是一个StringRep指针

image.png

image.png

component和container的生命周期是不同步的,可能先有container,等到需要使用指针的时候才会创建component

继承Inheritance

is-a关系

struct _List_node_base
{
  _List_node_base* _M_next;
  _List_node_base* _M_prev;
};

template<typename _Tp>
struct _List_node
  : public _List_node_base
{
  _Tp _M_data;
};
// _List_node继承自_List_node_base

image.png

image.png

  • 构造函数由内而外(先base):先调用Base的default构造函数
  • 析构函数由外向内(先derived)

base的析构函数必须是virtual,不然会出现undefined behavior

虚函数

  • non-virtual 函数:你不希望derived class 重新定义(override) 它.
  • virtual 函数:你希望derived class 重新定义(override) 它,且你对 它已有默认定义。
  • pure virtual 函数:你希望derived class 一定要重新定义(override) 它,你对它沒有默认定义。
class Shape {
public:
  virtual void draw( ) const = 0;  // pure virtual
  virtual void error(const std::string& msg);  // impure virtual
  int objectID( ) const;  // non-virtual
  ...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

转换函数

没有返回类型

class Fraction
{
public:
  Fraction(int num, int den=1): m_numberator(num), m_denominator(den) {}
  operator double() const {
    return (double)(m_numberator / m_denominator)
  }
}
private:
  int m_numberator; //分子
  int m_denominator; //分母
}
  • 上面的double函数就是没有返回类型,返回类型就是名称的那种类型,也就是double。前面加上operator,表示会自动的将Fraction分数类型转为double类型
  • 转换函数通常会是const,因为只是转换,并不会改变数据
Fraction f(3,5);
double d = 4 + f ; // f会调用operator double将f转为double类型 0.6

explicit函数

class Fraction
{
public:
  Fraction(int num, int den=1): m_numberator(num), m_denominator(den) {}
  Fraction operator+(const Fraction& f) {
    return Fraction(...)
  }
}
private:
  int m_numberator; //分子
  int m_denominator; //分母
}

上面的构造函数就是 non-explicit-one-argument ctor,因为ctor前无explicit关键字,且其中一个参数(den)有默认值

Fraction f(3,5);
Fraction d2=f+4; // 调用non-explicit ctor将4转为Fraction,然后调用 operator +

如果上述class加上转换函数

class Fraction
{
public:
  Fraction(int num, int den=1): m_numberator(num), m_denominator(den) {}
  
  // 加上double函数
  operator double() const {
    return (double)(m_numberator / m_denominator)
  }
  
  Fraction operator+(const Fraction& f) {
    return Fraction(...)
  }
}
private:
  int m_numberator; //分子
  int m_denominator; //分母
}
Fraction f(3,5);
Fraction d2=f+4; // Error ambiguous。因为编译器不知道把4转换为Fraction,还是把f转换为double,因为都是可行的,所以编译器会报错

构造函数使用explict关键字(该关键字通常也只在构造函数这里使用),告诉编译器,这个构造函数只能是真正创建对象的时候才能调用,不能默认转换。不要把4转换成4/1

class Fraction
{
public:
  explict Fraction(int num, int den=1): m_numberator(num), m_denominator(den) {}
  
  operator double() const {
    return (double)(m_numberator / m_denominator)
  }
  
  Fraction operator+(const Fraction& f) {
    return Fraction(...)
  }
}
private:
  int m_numberator; //分子
  int m_denominator; //分母
}
Fraction f(3,5);
Fraction d2=f+4; // Error conversion from 'double' to 'Fraction'。因为"+"运算符两边得是Fraction,而现在的4已经不能转换成4/1了,所以+方法会报错

STL容器库

六大部件

image.png

  • 容器:容器内放item,东西占用内存,内存由容器通过分配器来管理。
  • 分配器:用于容器内,对item进行内存分配,一般不写有默认值。自己写的话需要与容器内的item大小相匹配
  • 算法:对容器内的item进行操作,提取成独立模板函数,这就是算法
  • 迭代器:算法通过迭代器这个桥梁,来操作容器内的item。迭代器可以想成泛化指针
  • 适配器:用于转换函数、
  • 仿函数:
#include <vector>
#include <algorithm>
#include <functional>
#include <iostream>

using namespace std;

int main()
{
    int ia[6] = {27, 21, 12, 47, 151, 88};
    vector<int,allocator<int>> vi(ia,ia+6);

    count << count_if(vi.begin(), vi.end(), not1(bind2nd(less<int>(), 40)));

    return 0;
}
  • vecotr vi就定义了一个vector的容器

  • <int, allocator<int>>中的allocator就是一个分配器,分配int

  • vi.begin()就是iterator,一个迭代器

  • count_if是一个算法

  • bind2nd是一个函数适配器,less本来是一个函数用于比较大小,这里用函数适配器,将第二个参数(也就是比较中的一个)绑定成40这个值, bind2nd(less<int>(), 40)就是找小于40的值

  • not1是一个函数适配器,表否定,与后面的函数结合一起就是用于否定判断,最后变成"若>=40,则为true"。

容器分类

序列式容器
  • Array:固定长度的连续空间内存
  • Vector:单向可扩充内存。当容器内空间不够时,会以2倍的大小向后扩充(扩充并不是在原地址扩充,而是找到一块新地址,再把内容复制过去)
  • Deque:双向可扩充内存。分段连续。Deque里存在一段内存,里面存放的都是一个指针,每个指针指向另外一段内存buffer,每段buffer都是连续的。各段buffer最后都通过指针顺序连接起来。所以外界看起来整个都是连续的

image.png

  • List:链表,通过双向指针来连接每一块内存。每增加一个新内容都通过指针来连接起来
  • Forward-List:单向链表,通过单向指针连接内存
  • ...
关联式容器

底部都是红黑树结构

  • Set/Multiset:Multiset是可重复放置item,Set不能重复
  • Map/Multimap:同上,存放key:value
  • ...
无序容器
  • ...