侯捷老师C++学习笔记——大气编程(上)

本课程要有一点点C或C++的基础,学习效果会更好哦。
侯捷老师讲的特别通透,听完收获很大。

Lesson1 简介

课程基础:

  • 曾经学过某种面向过程的编程语言(procedural language)

image-20211110062021618

  • 知道程序的编译链接过程

  • 知道如何编译链接,如何建立一个可执行程序

课程目标:

  • 培养正规大气的编程习惯

  • 以良好的方式编写C++ Class(对于单一的类,我们叫做基于对象的编程)

    • 有指针的Class
    • 没有指针的Class
  • 学习Class之间的关系(对于多个Class之间的关系,称为面向对象的编程)

    • 继承(inheritance)
    • 复合(composition)
    • 委托(deligation)

学会了一种语言,编程思想掌握了,除了具体语法有一些差异,其他语言也就学会了。

C++包括:

image-20211110064133887

标准库很重要,一个C++程序员一定会用标准库。

C++推荐书籍:

image-20211215005323692

还有Effective C++ ,STL源码剖析。

本课程有两个例子,一个是写一个复数complex,另一个是写字符串string。后面都是用到这两个例子,就不再多说明了。

Lesson2 头文件与类声明文件

image-20211110064729929

一个程序中,包括数据和函数,函数是用来处理数据的,但是在C中,数据是全局的,各个函数都可以去处理它们,C++的思想就是把数据和处理这种数据的函数包在一起,这就是类。类是C语言中的Structure的升级,带有一些新的特性。

带指针的class和不带指针的class

不带指针的class,如复数,class创建了多个对象,这些对象中的数据部分占用的大小是一样的,然后用class中的函数可以处理这些数据,注意这里的函数只有一份,不是每个对象里都有一个函数。

而带指针的class,如字符串,每个字符串的长度不同,所以大小也是不同的,class中的数据类型是固定的,大小不可以变化,所以class中定义了指针,用指针指向字符串,然后每个指针占用的大小是相同的。

代码的基本形式

包括:

  • 头文件
  • 主程序

其实是一样的,只不过有角色的区分,所以分为头文件和主程序。

引用标准库的头文件用<> ,自己写的用""

而且,头文件的延伸文件名不一定是.h .cpp,也可能是.hpp或其他的扩展名 。

Eg. complex.h

#ifndef __COMPLEX__
#define __COMPLEX__


#endif

上述的代码是一个防卫式声明,防止头文件被反复的包含。

头文件布局

image-20211215005426587

头文件最主要的是上图中的1和2,1是类的声明,就是声明一下类中有哪些变量,哪些函数。2是类中函数的具体定义。但有时在做1和2时,一些东西应该先声明过,所以这就有了0前置声明。

class语法部分

class complex		//class head
{								//class body
  public:
  	complex (double r=0,double i=0):re(r),im(i)
    {}
  private:
  	double re;
  	double im;
};

如上,类的声明部分,要有class head,也就是类的名字,然后下面的大括号里面是class body。

模板

下面我们慢慢发展我们的代码。

看上面类中的数据部分,re和im是double类型的,但是如果我们想要写成别的类型时,也不能反复的写好多个类呀,所以这时候就可以用到模板,这样我们就可以在定义对象的时候再制定里面数据的类型了。

template <typename T> //使用模板,要在类前加上这句话,这里的T是一个符号,任何符号都行。这就是告诉编译器T是一个模板。
class complex		
{
  private:
  	T re,im;
};

在用的时候,我们就可以把T替换成我们制定的类型。

complex<double> c1(2.5,1.5);
complex<int> c2(1,2);

Lesson3 构造函数

我们现在不再介绍模板,回到我们的代码继续发展。

内联(inline)函数

在声明类的时候,我们也可以直接在类中定义函数,这种定义就叫做内联函数。

内联函数的好处就是调用的时候很快,但是是否是内联函数还要由编译器决定。编译器会根据函数的复杂程度自行决定。

class complex		
{								
  public:
  	complex (double r=0,double i=0):re(r),im(i)
    {}
  	
  	double real() const{return re;}//这个函数很简单,很有可能被编译器编程内联函数。
  	double imag() const{return im;}
  private:
  	double re;
  	double im;
};

上面的代码是直接在类的声明的时候定义了函数。我们也可以在后面具体定义函数的时候定义内联函数,此时就要加上关键字inline

inline double complex::imag() const{return im;}

访问级别

在class body中,定义了访问级别。主要有public,private和protect。

主要介绍public和private。

public就是公开的,外部可以看的到。

private就是只有该class里可以看的到,一般数据部分会放到private里。

函数也分为类自己的函数还有给外部用的函数。

在写代码时,各个段落是可以互相交错的。先public再private然后再public。

构造函数

创建一个对象,构造函数会被自动调用。

complex c1(2,1);//赋初始值
complex c2;//不赋值,默认创建
complex* p = new complex(3,1);//动态创建,创建了一个该类的指针

构造函数名一定要和类的名字相同。

函数的参数是可以有默认值的,在创建对象的时候没有指明参数,就使用默认值。

构造函数是没有返回类型的。

class complex		
{								
  public:
  	complex (double r=0,double i=0):re(r),im(i)
    {}
};

上面的写法是一种大气规范的写法。只有构造函数有这种写法。

re(r)im(i) 就是把r和i的值赋值给class的数据。当然也可以写到大括号里,但是这样的写法不好。

不带指针的类一般不用写西沟函数。

重载

构造函数可以有多个重载。因为要被初始化,可能有很多种情况的设定,所以需要不同方法的构造函数。

当然一般的函数也可以重载,事实上,在我们写代码的时候,函数名是一样的,但是当进行编译的时候,由于传入的参数类型,个数还有返回值的类型不同,编译器编译后的代码显示的函数名是不一样的。只是我们人类在看的时候是一样的。

下面这种重载是不允许的:

class complex
{
  public:
  	complex(double r=0 , double i=0):re(r),im(i){}
  	complex()re(0),im(0){}//这两种写法是不行的,编译后的代码是一样的,这时编译器就不知道调用哪个函数了。
  private:
  	double re;
  	double im;
};

Lesson4 参数传递与返回值

构造函数一般不可以被放在private,如果放在private里,就说明外界不可以创建对象。

有一种设计模式叫做singleton就是把构造函数写到private里。

常数成员函数

class中的函数有两种,会改变数据内容的和不会改变数据内容的。

不改变数据内容的函数一定要加 const ,这是大气的编程习惯。

class complex
{
  public:
  	complex(double r=0 , double i=0):re(r),im(i){}
  
  	double real () const {return re;}
  	double imag () const {return im;}
  
  private:
  	double re;
  	double im;
};

函数名后加 const 说明该函数一定不会改变class的数据。如果不加,就说明可以改动。

那么不加会有什么后果呢?

看下面的例子:

complex c1(2,1);
cout<<c1.real();
cout<<c1.imag();

const complex c2(1,1);//这里说明该对象是一个常量,内部的数据一定不会改
cout<<c2.real();//那么在调用该函数的时候,如果函数没有加const,就说明内部的数据可以该,这就与定义时相矛盾
cout<<c2.imag();

函数的参数传递

1.pass by value

pass by value就是把要传入的变量的值整包传过去,这个变量有多少个字节,就传多少个。这样的效率很低。

2.pass by reference

在C中,可以用指针来传递,就是把这个变量的地址传进去。在C++中,可以用reference(引用)来进行传递。

其实引用的底层就是传指针。引用的形式很漂亮。

class complex
{
  public:
  	complex(double &r=0,double &i=0):re(r),im(i){}//这就是传引用
  private:
  	double re;
  	double im;
};

如上面的例子,传引用就相当于 double & r = varies ,这时,就相当于为varies变量起了一个别名r,当改变r的时候,varies也会改变。r的地址和varies的地址一样。

当不想让varies改变时,就可以在r前面加上 const 关键字。此时改变r的值时,编译就会出错。

参数传递尽量都传引用。

返回值的传递

1.return by value

double function()

2.return by reference

double& function()

什么时候不可以return by reference呢?

当返回的东西是函数内创建的,这是一个local变量,当函数运行完之后,就直接消失了,所以此时renturn by reference,指向到一个消失的东西。这就不可以了。

友元

class中的数据在private下,外界不可以获取。但是想让一些函数可以直接获取,就可以加上 friend 关键字,表明这个函数是该class的朋友,可以直接获取class中的数据。

定义友元函数,要在class中声明一下子。

class complex 
{
	public:
	
	private:
	
  friend complex& __doap1(complex*,const comp1ex)//在class中声明一下子,该函数是该class的友元,写在哪个标签下都无所谓
};

inline complex& __doap1(complex* ths,const comp1ex& r)//定义函数
{
  
}

要注意:相同class的各个object互为友元,这就是说对象1中的函数可以直接处理对象2中的数据。

Lesson5 操作符重载与临时对象

事实上,在C++中,操作符就是一种函数,是可以重新定义的。也就是操作符重载。

操作符重载

操作符重载的函数可以写成成员函数的形式,也可以写成非成员函数的形式

1.成员函数的形式

这就是把操作符重载函数写在class内,对该class的object进行操作。

首先我们来看一下编译器是如何看待操作符的。

image-20211110090019793

+= 这是一个二元操作符,就是有两个操作数的。

编译器看待这个符号,就是把这个符号作用在左边身上,如果左边对这个符号进行了定义,那么编译器就找到了这个函数。

所有的操作符重载都有一个隐藏的参数,就是this,谁调用这个函数,那个谁就是this,this是那个谁的指针。这里就是c2的指针。

在写重载的时候,这个this就不写在形参列表里(也不能写,写出来就错了),但是是有的,如下面的代码:

inline complex& complex::operator +=(const complex& r)
{
  return __doap1(this,r);//这个函数的功能是修改this指向变量的值
}

所有的二元操作符都是这种规则。

在这里补充一下by reference传递的另一个好处,就是传递者不需要知道接收者的形式,如果要用指针进行传递,一定要搞清楚,接收者接收的是指针还是value,因为接收者如果是一个指针,那么它只能接收指针,所以传递的时候要取地址。

注意上面代码,操作符重载函数有一个返回值类型,complex& 。虽然该函数可以直接通过this为操作符前面的变量赋值,有没有这个都无关紧要。但是下面这种形式,就需要有返回值了。

c3+=c2+=c1

2.非成员函数的写法

之前的+=操作符重载成员函数写法,函数的形参里面自动包含了一个隐藏的this指针,可以直接操作this,从而改变操作符左边的变量。

但是有的时候,不是改变操作符左边的变量。

如:

c3=c1+c2;
c2=c1+5;

此时,就需要使用非成员函数的写法。

image-20211215005524527

这里是没有this的存在的。

这种情况必是return by reference ,因为返回的必定是local object。

临时对象 typename()

在上面的return中,我们可以看见,complex (,) ,这就是一个临时对象。

这种形如 typename () ,是临时对象。它的声明到下一行就结束了。

例如熟悉的int类型,临时对象就是:

int(5);//这并不是一个函数,就是一个值为5的int型的临时变量,当运行到下一句话时,这个变量就消失了。

Lesson6 复习complex

本节课是复习课,回顾了complex类的创建过程。

Lesson7 三大函数

介绍完complex类之后,我们现在继续学习带指针的类的写法。以string字符串这个类为例子。

在创建一个新的字符串时,我们会遇到以下的几个情况:

int main()
{
	String s1();//创建一个没有初值的对象
	String s2("hello");//创建有初值的对象
	
	String s3(s1);//创建一个新的对象,以另一个对象为初值,是一个拷贝的动作
	s3=s2;//赋值,也是一个拷贝的动作,这里与上一句的区别就是这里的s3不是第一次出现
	
}

在之前的complex的例子,我们没有写,但是编译器会自己制定一个规则,就是一项一项的对应,去拷贝和赋值。但是复数是很规则的,有实部和虚部,默认的拷贝和赋值完全可以满足。

但是字符串创建的是指针,两个指针不能指向同一个地方,这样会弄乱,所以带指针的类一定要自己写拷贝和赋值。

下面我们来看String类

class String
{
  public:
  	String(const char* cstr=0);
  	String(const String& str);
  	String& operator=(const String & str);
  	~String();
  	char* get_c_str() const{return m_data;}
  private:
  	char* m_data;
};

字符串就是指针指着头,最后有一个结束符号。

我们先来看第一个构造函数,是没有初始值得构造函数。函数的代码如下:

inline String::String(const char* cstr=0)
{
  if(cstr)
  {
    m_data = new char[strlen(cstr)+1];//如果指针不是空,那么运行这段话,分配一个空间,大小为字符串的长度再加上1,因为后面还有一个结束符。
    strcpy(m_data,cstr);//然后调用这个函数,把指针地址拷贝到数据中。
  }
  else
  {
    m_data=new char[1];//如果是空指针,那么就在该空间放一个结束符。
    *m_data='\0';
  }
}

//该构造函数应对下面这两句话,会被调用
{
	String s1();
	String s2("Hello");
  String*p = new String("Hello");//这里也会调用上面的构造函数。
}

析构函数

当这个对象被清理的时候,会自动调用析构函数,需要把这块的内存给释放,否则就内存泄漏了。

析构函数:

inline String::~String()
{
	delete[] m_data;
}

拷贝构造

三大函数有拷贝构造,拷贝赋值,析构函数。析构函数我们上面说了。下面说前两个。

只要是带有指针的类,就要有这三大函数。

如果没有拷贝构造函数,用编译器自动生成的拷贝构造,就会发生浅拷贝

浅拷贝就是,自动生成的拷贝构造函数,只会把指针本身拷贝过去,这样就会造成两个指针指向同一个东西。

inline String::String(const String& str)
{
  m_data=new char[strlen(str.m_data)+1];
  strcpy(m_data,str.m_data);
}

//拷贝构造适用的条件
{
  String s1(s2);
  String s3=s1;//这个虽然是赋值符,但是s3是新建立的,此时也是调用拷贝构造函数。
}

拷贝赋值

拷贝赋值的过程:

  • 先将原来的东西清空
  • 然后再拷贝字符串
inline String& String::operator=(const String& str)
{
  if (this==&str)return *this;//自我检查,看是不是自己赋值自己。
  delete[] m_data;//删除原来的空间内容
  m_data=new[strlen(str.m_data)+1];//重新指向一个空间
  sctrcpy(m_data,str.m_data);//拷贝内容
  return *this;//返回自己,这样可以连=
}

**注意:**上面的自我检查很重要,如果是自己赋值自己,没有自我检查,会先把原空间的内容删掉,结果赋值后字符串的内容变成空了。

Lesson8 内存管理

栈(Stack)

是存在某作用域的一块内存空间,例如调用的函数,函数本身会形成一个栈来放置它所接收的参数,以及返回地址。

在函数本体(function body)内声明的变量所使用的内存块都取自上述的栈。

该空间中的变量会被自动清理掉。

堆(Heap)

是操作系统提供的一块全局(global)的内存空间,程序可动态分配(dynamic allocated)该空间。

所以我们需要手动开辟出一块空间,并且手动清理该空间。

生命期

  1. stack object:其生命在作用域结束之后结束。
  2. static object:其生命在作用域结束之后仍然存在,直到整个程序结束。
  3. global object:其生命也是在程序结束之后才结束,也可以把它当成是一个static object。

内存管理

1.new

image-20211110140453980

如果不delete,那么随着程序的运行,指针p会消亡,那剩下所指的空间就没人管了。所以称为内存泄漏。

分配内存的分解动作:

image-20211215005626031
  1. 先调用一个叫operator new的函数,分配内存,得到一个指针。
  2. 然后将这个指针转型。
  3. 然后通过指针调用该类的构造函数。

2.delete

再来分解delete的动作:

image-20211110141305647
  1. 首先调用析构函数。
  2. 释放内存。

良好的编程习惯:

new[] 要搭配 delete[]使用。

Lesson9 复习String

本节课主要是复习之前的String类。

Lesson10 类模板 函数模板 及其他补充

static

每创建一个对象,对象中的非静态数据都会被创建一份。

而函数只有一份,不同的对象调用成员函数,其实就是给函数传入不同的地址,让成员函数来处理该地址对应对象的数据。这靠的就是this pointer。

可能有点绕,好好想一下。

image-20211215005802555

函数的参数列表中一定不要写this,编译器会自动加,否则就会报错。黄色的地方呢就是可加可不加。

当成员变量加上static关键字,就和对象脱离了,在内存中只有一份。

而静态成员函数和一般的成员函数几乎一样,都只有一份。但是唯一的区别是,静态成员函数没有this pointer。所以它不能直接去处理对象中的数据,而是去处理静态成员变量。

静态变量的使用:

静态变量在类中声明,但是在使用的时候,需要在类外进行定义。

这里辨析一下声明和定义。无论是变量还是函数。声明是告诉编译器有该东西存在,但是不分配内存。

而分配内存,就称为定义。

class Account
{
  public:
  	static double m_rate;
  	static void set_rate(const double& x){m_rate=x;}
};
double Account::m_rate=1;//这里是定义,可以赋初值也可以不赋值。
int main()
{
  Account::set_rate(5);//调用静态函数可以直接调用,不通过对象。
  Account a;
  a.set_rate(7);//也可以通过对象来调用。
}

小贴士:凡是类里面的一切东西,无论是变量还是函数,拿到外面的名字都要加上类名。就是在家里可以直接叫名字,但是到外面,就必须要加上姓氏。

就像上面的 static double m_date,在外面的名字就是 Account::m_date

静态函数的使用:

  • 通过对象调用。
  • 通过类名调用,也就是直接用函数的全名调用。

Singleton设计模式

只能创建一个对象。

class A{
	public:
		static A& getinstance(return a;);
  	setup(){}
  private:
  	A();
  	A(const A& rhs);//构造函数都写在私有里,外界不能创建。
  	static A a;//这里创建了一个静态变量,没有任何人创建A的对象时,就已经存在一个了。
};

如果要使用这个对象,就通过静态函数来获取。

A::getinstance.setup()//通过静态函数便可以操作这个对象的所有成员函数。

这样写呢也有一个弊端,就是在没有使用这个对象的时候,它已经出现了。

所以有下面这种改进:

class A{
	public:
		static A& getinstance();
	private:
  	A();
  	A(const A& rhs);
};
A& A::getinstance()
{
  static A a;
  return a;
}

改进后,在静态成员函数内声明了这个对象,这样的好处就是当使用这个静态成员函数一次,该对象就会被创建,并且会一直存在到程序结束。

函数模板(function template)

template<class T>
inline const T& min(const T& a,const T& b)
{
  return b<a?b:a;
}
//在使用时,与类模板不同,不用再指出具体的类型
r3=min(r1,r2);

编译器会做实参推导,自动推导出类型。

例如上面的比大小,编译器会自动推导,a和b的类型,然后在下面有<,编译器会根据该类型寻找是否有重载。那么定义该类的人就需要写好重载<的函数。这样职责分开,是特别好的。在C++中标准库中,有很多这样的模板函数,称为算法。

namespace

namespace std
{
...
}

上面的例子是把所有的东西都包在std里面,这样可以避免同名的函数混淆。

在使用的时候,就要打开这种包裹。

using namespace std;//全开 using directive

using std::cout;//就开这一个 using declaration
int main()
{
  std::cin<<...;
  cout<<...;
}

Lesson11 组合与继承

前面我们学的是写一个单一的class,这种叫基于对象设计。

下面我们要学习,类和类之间的关系。也就是面向对象编程。

类与类的关系大致上可以分为三种:组合,委托,继承。

Composition组合(表示has-a)

一个class里面包含了另一个class的对象。

对于复杂的程序,通常会使用图来表示。组合关系的表示方法如下:

image-20211110162419332

下面讨论这种组合关系时的构造函数和析构函数。

构造函数

img

构造函数规则,由内而外,先调用内部包含class的默认构造函数。如果想调用别的构造函数,需要自己写明。

放一个慢动作:

Container::Container():Component(){}

内部的构造函数可以不用自己写,编译器会帮我们自动完成。

析构函数

由外而内,先调用Container的析构函数。再放一个慢动作:

Container::~Container(){~Component}

这也是编译器自动帮我们完成的。

Delegation委托(composition by reference)

类中包含另一个类的指针。

image-20211110164453307

在这里有一个很有名的设计模式,就是一个类中包含了另一个类的指针,而这个指针所指的类中包含了要实现的所有功能,这种模式称为 pimpl

image-20211215005930376

Inheritance继承(表示is-a)

继承的关系,子类可以包含父类的数据。

语法:

class A:public B//public公有继承,还可以是private protect
{};

用图表示:

image-20211110165524489

下面是子类,上面的是父类,由儿子指向父亲。

就如同生物学的那样,界门纲目科属种。一直往下分,每一个门都是一个界的一种。所以是is-a。

继承最有价值的地方是和虚函数搭配使用。

image-20211215010208419

父类的析构函数要是virtual

在这里我们只探讨public继承。

Lesson12 虚函数与多态

使用继承的时候,要搭配虚函数使用,效果最佳。

继承的时候,数据会被继承下来,在创建子类对象的时候,会分配内存空间。

成员函数也会被继承下来,但是函数是不会被分配内存空间的。函数的继承是继承函数的调用权。

什么事虚函数呢?就是在成员函数的声明钱加上virtual

成员函数可以分为三类:

  1. non-virtual函数:不希望子类重新定义(override)
  2. virtual函数:希望子类重新定义 virtual void function()
  3. pure virtual函数:子类必须重新定义 virtual void function()=0
image-20211110171243449

例如,定义一个形状的类。世界上并没有叫形状的形状,所以该类一定会有子类。

上面就会定义一个画出形状的函数,这个函数一定要被子类override。

我们还会给每个形状都附一个编号,这个和具体的形状无关,所以用non-virtual函数。

我们还要打出操作各种形状的一些信息,如果子类不特别说明,就用父类定义的内容。如果子类想单独说明,也可以重写,所以这里用virtual函数。

Lesson13 委托相关设计

本节课介绍了一些设计模式。

就用到composition, delegation,inheritance这三种模式,进行组合,来解决现实的问题。

其中delegation+inheritance的功能最强大。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值