About
侯捷手把手教学C++
1.简介
编写规范的class代码(object based,单一class);
学习class之间的关系(object oriented,多个class)—继承/复合/委托;
主要学习c++ 98/c++11,以c++ 98为主;
同时关注C++ 语言和C++标准库;
推荐书籍:C++ Primer, The C++ Programming Language (C++11), Effective C++ 11, The C++ Standard Library, STL源码剖析;
2.头文件与类声明
数据和函数包在一起打包成class,与struct有一些差别。
C++代码的基本形式:
- .h(header files, class声明);
- .cpp(#include<iostream.h>, #include “own_file.h”, 标准库和自己的引用方式不同);
- .h(标准库);
头文件防卫式声明(任意include头文件,不会重复include):
#ifndef __COMPLEX__ |
类声明:class head, class body;
class template(用的时候再指定,比如数据类型);
template<typename T>
3.构造函数
inline函数,在class内部定义完成,比较快,但是是否成为inline函数由编译器决定;
数据指定为private,只给函数内部使用,用的时候利用class内部函数传递,不能直接调用;函数大部分可指定为public,允许给外部使用,也可指定private,class内部使用;
constructor(ctor,构造函数),创建对象,程序自动调用;
complex c1(2,1); |
变量的初始化和赋值是两个阶段,能初始化的不要放在赋值,效率差一些;
不带指针的函数多半不用析构函数;
: re (r), im(i)
构造函数可以有很多个,overloading重载,也就是说同一名的函数可能不止一个,但是编译器会对其作出区分,实际名称并不相同。但是写的时候要注意,可能会造成冲突,导致编译器不知道调用哪个。
4.参数传递与返回
举例Singleton,说明确实有类中的函数是private的。
const
关键字,代表函数不会改变数据内容:double real() const {return re;}
,如果考虑到函数不改变data,一定要加上,避免编译时错误。
const
也可以用在对象面前,代表这个对象的指定的值是不可以改变的,const complex c1(2,1);
参数传递:pass by value / pass by reference (to const),最好参数传递都用引用,如果害怕会引用的值会改变,可以to const,const complex& param
,约定引用的值不要改变,否则编译出错。
返回值传递:return by value / return by reference (to const),返回值也尽量传reference。
friend (友元):friend
关键字指定的函数可以取private中的data,比从class内部函数拿会快些。
相同class的各个object互为friend。
设计类注意:
- 数据放在private,函数尽量放public,可被外部调用;
- 参数尽可能传reference,看状况加const;
- 返回值尽可能传reference;
- class body中需要加const就去加;
- 构造函数的特殊用法,init初始值尽可能去用它;
return by reference不能使用的一种情况,class body外的各种定义,应该是局部的变量,local variable去返回的时候不要用引用,因为程序一结束,这个空间就释放出去了。
操作符重载与临时对象
1.operator overloading(操作符重载,成员函数),this:
不能在参数列写this(成员函数都有this pointer),但是函数里面可以用,操作符传递的时候会自动给this传指针。比如c2 += c1
中,+=
这个操作符重载了,传递的时候c2自动给了this,你设计的时候不需要人为指定传给谁,只需要指定c1给谁就行了。
连续赋值,c3+=c2+=c1
,关注一下返回值,ppt中举这个例子提醒我们操作符+=
重载函数返回类型是complex&
,非代表无返回值的void
,因此支持了从右到左连续赋值操作。
2.操作符重载,非成员函数,无this:
ppt举例为了应对几种可能用法,写出几种函数方式。
temp object --typename(); 同时表明返回的是局部变量,return by value,而不是reference: return complex(x + real(y), imag(y))
,这里的complex()就代表一个临时对象。
举例关于正负号+, -
和运算符+, -
编译器通过参数个数区分。
对于特殊的操作符只能写成全局的非成员函数,比如cout <<
中的<<
,输出操作符。当然侯捷老师举例说你也可以用成员函数代替,但是这样就变成了c1 << cout
,c1放前面用this指针读取,cout是操作符重载函数的输入参数,虽然不会有啥错误,但是不符合常规习惯。
这里讲了cout的返回值不是void,因为可能会连续输出,返回值为ostream&,再一次强调好的程序设计思想。
再一次总结:初始化值;const;参数传递尽量考虑pass by reference;return考虑value或者reference;数据尽量放在private,函数一般放在public中。
复习Complex类的实现过程
防卫式常数定义;
class head + class body;
private写数据,public写函数(返回值方式,参数传递方式,初值列,成员函数(前面加class名称),考虑是否加const,利用友元关键字获取private数据);
inline函数(是否成为看编译器),传引用时注意变量是否为local variable;
可用临时对象返回;
#ifndef __COMPLEX__ |
inline complex& |
inline complex |
#include <iostream.h> |
三大函数:拷贝构造,拷贝赋值,析构
解析string类,带point numbers指针;
big three,三个特殊函数;
class String |
class with pointer members必须有copy ctor(拷贝构造)和copy op=(拷贝赋值),浅拷贝与深拷贝的问题。
拷贝赋值:检测自我赋值,先清空,再开辟,然后复制过来。
// 析构 |
堆,栈与内存管理
stack,是存在于某作用域scope的一块内存空间。比如函数本身就会形成一个stack。
heap堆,由操作系统提供的一块global内存空间,程序可动态分配,比如new
关键字,需要自己释放。
stack(auto) / static / global object
注意写法,防止内存泄漏;
new: 先分配memory, 再调用ctor(拷贝构造);
delete: 先调用dtor(析构),再释放memory;
独家讲解VC编译器动态分配(new)的内存块和分配所得的array,进一步指出array new 一定要搭配 array delete;
char(unsigned), short, int(unsigned), long(unsigned), bool, float(单精度,保留到小数点后7位), double(双精度,保留到小数点后16位)。
复习string类的实现过程
防卫式声明;
动态分配,利用指针(32位系统4个byte);
构造函数,拷贝构造,拷贝赋值,析构函数(把自己清干净,clean);
// 类声明 |
// ctor构造函数 |
// copy ctor 拷贝构造函数 |
// copy assignment operator拷贝赋值函数 |
扩展补充:类模板,函数模板及其他
static关键字,定义静态变量。静态局部变量保存在全局数据区(在函数内定义,多次调用函数只会初始化一次,但是只受此函数控制),而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。静态全局变量可以实现文件隔离,静态全局变量不能被其它文件所用 (全局变量可以),其它文件中可以定义相同名字的变量,不会发生冲突。(见此)
singleton,某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式?(见此)
类模板,函数模板;
namespace std 封装到单元,将库名称封起来,防止冲突。两种用法,一种是using directive: using namespace std;
, 另一种是using declaration: using std::cout;
, 一般一个文件代码如果不长,只有几百来行,可以直接用第一种。(见此)
// using directive方法 |
// using declaration |
组合和继承
object oriented programming, object oriented desugn (OOP, OOD).
继承(inheritance),复合(composition),委托(delegation).
1.复合:has-a, 意思是说一个类具有比较强大的功能,可以将其某些性能改造包装成一个新的类,adapter. (container—> component)
复合下的构造和析构:构造由内而外(打地基),析构由外而内(剥洋葱)。
2.委托:composition by reference, pimpl: pointer to implementation.(handle/body, 编译防火墙)
共享,reference counting,当其中某个人想要改变共享的内容,那么会单独拿出一份copy来给他改,不影响原先的共享内容。
3.继承: is-a, : public _List_node_base
,属于哪一类
继承下的构造与析构,基类与派生类。父类是子类的一种组成成分,类似复合,因此构造由内而外,析构由外而内(父类的析构函数必须是virtual的,否则会出现undefined behavior)。
虚函数与多态
inheritance with virtual functions.
non-virtual函数:不希望子类重新定义(override,覆写)它;
virtual函数:你希望子类重新定义(override覆写)它,且你对它已有默认定义;
pure virtual函数:你希望子类一定要定义它,你对它没有默认定义;
class Shape{ |
举例application framework中的template method(指的某一个函数),不是之前的C++模板(通过this调用子类覆写的虚函数)。
inheritance+composition关系下的构造和析构,子类既有继承又有component part(子类先构造父类的default构造函数,然后在调用component的default构造函数),或者子类是继承,父类有component part(先复合,再继承);
委托相关设计
delegation+inheritance:
讲设计模式,并推荐Design Patterns Explained Simply
file system, 文件系统;
prototype, 原型;
复习:
0.防卫式声明。
1.inline关键字
line的使用是有所限制的,inline只适合函数体内代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。定义在类中的成员函数缺省都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上inline,否则就认为不是内联的。(只是给编译器的建议)
2.函数构造函数初始化,比赋值快。
3.const关键字加在为改变数据值的表达式前面,限定为只读属性。
4.尽量返回引用,但是局部临时变量或者函数结束对象就死亡的不能返回其引用。
5.友元friend,相同类的各个实例对象互为friend,可以通过彼此的内部方法调用传入参数的内部私有成员变量。
6.this指针,操作符重载函数,成员函数,编译器自动读取,设计者不可自己指定作为声明,但是可以在函数中显式使用。
7.构造,拷贝构造,拷贝赋值,其中拷贝赋值记得检测自我赋值,否则内存出错。
8.new关键字,先分配足够的memory,再进行构造。delete关键字,先调用析构函数,然后释放内存。用了new array,也要注意用相应的delete array.
9.多类关系,复合,委托,继承,以及继承中的虚函数。
Part2-导读
part1是面向对象编程,part2是兼谈对象模型。
- 保持良好编程风格和素养基础上,探讨更多技巧;
- 泛型编程(generic programming)和面对对象编程(object-oriented programming)虽然分属不同思维,但它们正是C++的技术主线,所以本课程也讨论模板(template);
- 深入探索面向对象之继承关系所形成的对象模型(object model),包括隐藏于底层的this指针,vptr虚指针,vbtl虚表,virtual mechanism虚机制,以及virtual function虚函数造成的polymorphism多态效果;
讲解C++11中的三个特性,更多的特性在侯捷老师另一个C++ 2.0教学视频中提及。
- variadic template;
- auto;
- range-based for loop;
转换函数 conversion function
大意就是类型之间的转换,比如把自定义的类类型转换成内建类型(比如double),后者向相反的方向转。(类转出去,或者把其他东西转换成类)。
只要设计者认为合理,就可以在一个类中写好几个转换函数。
转换函数的形式:operator typeName()
- 转换函数必须是类的成员函数;
- 转换函数不能指定返回类型,但是有返回值;
- 转换函数没有参数;
class Fraction{ |
non-explicit one argument constructor
explict关键字,用在构造函数前面,让编译器不去隐式调用变量类型转换,避免在用户不需要类型转换时就自动进行转换。
具体解释示例见此博客。
pointer-like classes
1.关于智能指针。
举例shared_ptr,实际上还有unique_ptr, auto_ptr(c++ 98, 但也可继续使用), weak_ptr等。
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同,shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种智能指针都定义在memory头文件中。
必须写操作符重载operator*()
和operator->()
其中箭头符号->会继续作用下去。
关于C++11中四种典型的智能指针讲解,见此。
2.关于迭代器。
容器,容器一定带迭代器
也是智能指针,但是略有不同,操作符重载多了几个,operator*()
,operator->()
, operator++()
, operator--()
,因为指针要移动。
迭代器是为了表示容器中某个元素位置这个概念而产生的,是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而非下标进行操作,因为标准库(STL)为每一种标准容器定义了一种迭代器类型,而只有少数容器支持下标操作访问容器元素。
function-like classes
仿函数,functor。
operator (), 重载小括号
仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator() 运算符。因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。
在stl中提供了大量有用的仿函数,比如plus,minus,multiplies,divides,modulus,equal_to,not_equal_to,greater…很多很多,根据传入的参数的个数我们可以分为只需要接受一个参数的仿函数(unary_function)和需要接收两个参数的仿函数(binary_function)。
为什么需要仿函数,这里给出了一个解释。
仿函数的一些应用见此。
namespace经验谈
包住你的函数,防止和项目中其他开发者导致类,函数冲突。
using namespace std;
namespace jj01(自己取得空间名称)
调用函数时,加上空间名,比如:jj01::test_member_template()
template 模板
class template,在part1中已经讲过,比如声明类的时候可以指定数据类型。
function template,与类模板不同的是,函数模板在使用是不需要显式地声明传入参数类型,编译器将自动推导类型(编译器会对function template进行实参推导)。
member template,成员模板。一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种函数称为成员模板。注意!成员模板不能是虚函数。该语言特性往往被用来支持class template内的成员之间的自动类型转换。
specialization
模板的泛化与特化。
还有一种partial specialization, 偏特化----个数上的偏特化,范围上的偏特化。
模板为什么要特化,因为编译器认为,对于特定的类型,如果你对某一功能有更好地实现,那么就该听你的。
模板分为类模板与函数模板,特化分为全特化与偏特化。全特化就是限定死模板实现的具体类型,偏特化就是模板如果有多个类型,那么就只限定为其中的一部分,其实特化细分为范围上的偏特化与个数上的偏特化。(C++模板全特化之后已经失去了Template的属性了)
模板的泛化:是指用的时候指定类型。
优先级:全特化>偏特化>主版本模板类。
template template parameter
模板参数本身也是模板。
容器需要好几个模板参数。
函数模板不支持模板的模板参数。
template<typename T, |
注意,模板的模板参数中,class 不能被替换成typename。在模板中,如果不能区分的哪应该使用typename和class,可以全部都用class替代,typename和class的作用基本相同,而typename出现得比较晚。同时它也支持缺省值(参考此博客)。
关于C++标准库
侯捷老师建议全部上手调用一遍,实验一遍。
可参考侯捷老师的C++标准库STL讲解视频。
三个主题
1.variadic templates (since c++11),数量不定的模板参数。
template <typename T, typename... Types>
…就是一个所谓的pack包,sizeof...(args)
可以看出几个arguments.
2.auto (since c++11)
让编译器帮忙推导type类型,但是频繁使用会使代码阅读性变差。另外有些情况不适合用auto,因为编译器可能无法判断类型。
3.range-based for (since c++11)
for (decl : coll){ |
for (int i: {2,3,5,7,9,13,17,19}) { |
vector<double> vec; |
更多的C++11特性讲解见侯捷老师的这个视频。
reference
一种漂亮的pointer,多半用在参数传递上面。指针*指向变量的地址,引用&可以理解为变量的另一个名称,代表的就是这个变量,虽然内部依旧是指针实现的。
复合&继承关系下的构造和析构
复习前面的类与类的关系。
继承:构造由内而外,析构由外而内。
复合:拥有关系,构造由内而外,析构由外而内。
继承+复合:base<----derived----->component, 具体怎么排base和component看编译器。
关于vptr和vtbl
object model对象模型,virtual pointer(vptr)和virtual table(vtbl).
多个虚函数只有一个指针。
动态绑定(指针调用,向上转型,调用虚函数),非静态绑定,虚机制,多态(面向对象继承多态的本质)。
可参考此博客。
关于this
除了上面的那个多态,虚函数的另一种用法,template method。此例子在part1也举过,放在这里由于之前解释了虚函数和动态绑定的用法,所以复习之后进一步加深这个设计理念。
后面简单从编译后的汇编码看静态绑定和动态绑定。
谈谈const
侯捷老师强调const关键字在设计时非常重要,一定要注意,该加的一定要记得加上。
const放在成员函数后头,可修饰成员函数。下面PPT的例子中,有两个operator[]
函数,由于const也算是函数签名,所以并不会构成ambiguity. 其中前者是针对常量字符串,不必考虑COW(copy on write),后者需考虑COW,在这里,函数是从一个字符串中取出某个值,可能对其进行变动,返回是reference也预示了这个情况。侯捷老师在这里举例了前面的共享内存的例子,可能多个指针变量指向同一块内存,如果其中某一个需要修改所指的变量值,那么就是单独copy出来一份给他改。
关于const关键字的简单总结可参考此博客。
关于New, Delete
1.复习
new先分配memory,再调用ctor.
delete先调用dtor,再释放memory.
array new一点更要搭配array delete.
即new()
搭配delete()
,new[]
搭配delete[]
.
2.重载operator new, operator delete, operator new[], operator delete[]
从内存分配上看,new[]里面还加了一个counter,4个字节大小,存储你new了几个元素,这样便于delete时销毁。
如果写上了global scope operator ::,那么调用new和delete会绕过前述所有的overload functions,强迫使用global version.
C++语言内置默认实现了一套全局new和delete的运算符函数以及placement new/delete运算符函数。不管是类还是内置类型都可以通过new/delete来进行堆内存对象的分配和释放的。对于一个类来说,当我们使用new来进行构建对象时,首先会检查这个类是否重载了new运算符,如果这个类重载了new运算符那么就会调用类提供的new运算符来进行内存分配,而如果没有提供new运算符时就使用系统提供的全局new运算符来进行内存分配。内置类型则总是使用系统提供的全局new运算符来进行内存的分配。对象的内存销毁流程也是和分配一致的。
new和delete运算符既支持全局的重载又支持类级别的函数重载。下面是这种运算符的定义的格式:
//全局运算符定义格式
void * operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);
//类内运算符定义格式
class CA
{
void * operator new(size_t size [, param1, param2,....]);
void operator delete(void *p [, param1, param2, ...]);
};对于new/delete运算符重载我们总有如何下规则:
- new和delete运算符重载必须成对出现。
- new运算符的第一个参数必须是size_t类型的,也就是指定分配内存的size尺寸;delete运算符的第一个参数必须是要销毁释放的内存对象。其他参数可以任意定义。
- 系统默认实现了new/delete、new[]/delete[]、 placement new / delete 6个运算符函数。它们都有特定的意义。
- 你可以重写默认实现的全局运算符,比如你想对内存的分配策略进行自定义管理或者你想监测堆内存的分配情况或者你想做堆内存的内存泄露监控等。但是你重写的全局运算符一定要满足默认的规则定义。
- 如果你想对某个类的堆内存分配的对象做特殊处理,那么你可以重载这个类的new/delete运算符。当重载这两个运算符时虽然没有带static属性,但是不管如何对类的new/delete运算符的重载总是被认为是静态成员函数。
- 当delete运算符的参数>=2个时,就需要自己负责对象析构函数的调用,并且以运算符函数的形式来调用delete运算符。
更多详解可参考此博客。