简介
此文档为本人边看视频边记的笔记,方便以后自己回看笔记时能快速想起课程内容,详细内容请看老师讲的视频。
课程来源:GeekBand,也可以在b站搜到.
如果不能显示出图,请看Github的博文。
C++ 编程简介
基于对象(Object Based)
- class without pointer members -Complex
- class with pointer members -String
面向对象(Object Oriented)
- 学习Classes之间的关系
- 继承(inheritance)
- 复合(composition)
- 委托(delegation)
C++历史
C语言(1972)
C++(1983)早期叫C with Class
C有很多版本,如 C98,C03,C 11,C++14
学习C++(其他语言也是)可以分为两个部分:
- C++语言
- C++标准库
经典的书:
- C++ Primer (C++第一个编译器的作者写的)
- C++ Programming Language (C++之父写的)
- Effective C++ (专家写的,以条款方式写的)
- The C++ Standard Library(写的C++的标准库)
- STL源码剖析(较深入的将标准库)
头文件与类的声明
C vs C++
C语言数据和函数是分离的,数据都是全局的。这样编程有很大影响。后来发展的C是数据和函数合在一起成为class,C的struct几乎等同于class,只有微小的差别。
前面讲过的class可以分为带指针和不带指针的,典型的例子就是complex(不带指针)和string(带指针)
- Object Based : 面对的是单一class 的设计
- Object Oriented : 面对的是多重classes 的设计,classes 和classes 之间的关系。
C++的代码基本形式
注意头文件:<>对应标准库,“”对应自己写的东西
头文件中的防卫式声明(guard)
1 |
|
可以避免包含头文件时的顺序问题。
头文件的布局
1 |
|
class的声明
1 | class complex//class head |
使用时
1 | { |
上述代码中复数只有double的实部和虚部,为了可以支持多种类型并且不增加类,我们引入模板。
1 | template<typename T>//T也可以是其他字母 |
使用时:
1 | { |
内联函数、访问级别、构造函数
inline(内联)函数
1 | class complex |
函数在class本体里定义就形成了inline,inline像宏一样,函数如果是inline的,就会很快,所以所有的函数都写成inline是最好的,然而编译器没有能力把所有函数都做成inline,一般来说程序复杂,编译器就不能做成inline。inline function最后能不能做成inline看编译器的能力。
不在本体里定义inline的要加inline:
1 | inline double |
访问级别(access level)
- public :外部能看到的
- private:只有自己能看到的,一般只有内部函数处理的数据要用private
- protect:受保护的
错误示范:
1 | { |
正确示范:
1 | { |
构造函数(construct,ctor)
如果创建一个对象,构造函数会自动调用起来。没有程序里直接调用函数的写法。
如以下三种方法创建对象:
1 | { |
则会自动调用之前写的代码:
1 | complex (double r = 0, double i = 0)//0为默认实参(default argument) |
构造函数的写法很独特,他的函数名称一定要和类的名称相同。
构造函数没有返回值类型,因为不需要有,因为构造函数就是要创建类的。
注意默认值并不是构造函数特性,其他函数也可以写成默认值。
initialization list是只有构造函数才有的写法。
如上面的代码也可以写成:
1 | complex (double r = 0, double i = 0) |
两种写法结果是一样的,但是一般有过良好编程习惯的会写成第一种。
第一种效率会好一些,背后有很多原理,简单来讲可以说是因为变量数值的设定有两个阶段,一个是初始化,一个是赋值。第二种写法就是跳过了初始化,效果差。
相对于构造函数有一个析构函数,不带指针的类多半不用写析构函数,如上述所写的Complex。
重载 overloading
构造函数可以有很多个重载。
重载:同名的函数名称有很多个以上。重载常常发生在构造函数身上。
图中的两个构造函数是不能同时存在的,因为在创建对象时,无法知道应该调用哪个构造函数。
同名的函数在编译器处理后会变成其他名字。这个名字是给机器看的。编译器会把函数的名称,参数有几个,参数是什么类型编码,机器会区分出同名的不同函数。
构造函数也可以放在private里,这是不能按照之前的方法创建对象。详细的使用方法这里先不讲。但要知道有这种写法。
如设计模式Singleton:
1 | class A { |
1 | A::getInstance().setup(); |
参数传递与返回
定义函数是确定是否添加const
1 | class complex |
注意
1 | double real () const { return re; } |
函数中的const是十分重要的,很多人会忽略。
const在此处的表示函数只是将数据取出,并不改变数据内容。
class的函数可以分为会改变函数内容的和不会改变函数内容的。不会改变数据内容的函数要加加const。
开发人员在设计接口时就要确定是否要加const。
如果不加会产生什么后果:
1 | { |
上面的const说明使用者复数中的2,1是不能变的,假如定义时不加const,说明这个函数可能会改数据,这样在定义和调用就发生了冲突。
参数传递(pass by value vs. pass by reference(to const))
1 | class complex |
- by value :是数据整包传过去,数据有几个字节就传几个字节
- by reference(引用):带&,传引用相当与传指针的速度。
- by reference(to const):没有const时,传给对方的数据,对方可能修改后影响我,如果不想自己受到影响,就要加上const,表示此引用不会该改变数据,对方改变就会出错。
良好的习惯:参数传递尽量的传引用,速度快
返回值传递(return by value vs. return by reference(to const))
上述代码中
1 | complex& operator += (const complex&);//return by reference |
友元 friend
1 | inline complex& |
相同class的各个object互为friends
使用场景
看下面的例子do assignment plus(__doapl)
1 | inline complex& |
上述代码中的第一个参数(ths)会发生改动,则不能加const,第二个参数®不会发生改动,可以加const。
在函数里面创建的东西不能加引用往外传,因为那是局部的,传到外面就消失了,其他情况都可以传引用(inline complex&)。
操作符重载(operator overloading)
C++中操作符其实就是一种函数,也可以重载。
成员函数重载
所有的成员函数都带有一个隐藏的参数(this),谁调用,谁就是this。this是一个指针,下面是一个复数+=运算的例子。
传递者无需知道接收者是以reference形式接收,上述代码中编写
return *ths;时不必考虑返回的是以什么形式接收的。
此外,complex::operator +=的返回 complex&,也可以返回void,这时执行,c2+=c1没有问题,但是如果连续赋值c3+=c2+=c1;就会有问题,所以严谨写法为返回complex&。
非成员函数重载(无this)
1 | inline complex |
注意上述代码不能return by reference,因为他们的返回必定是local object。
complex()的语法是,temp object(临时对象), typename();
1 |
|
cout是一种对象,ostream.
拷贝构造,拷贝复制,析构
- Class with pointer member(s):string
1 |
|
1 | int main() |
complex等不带指针类的拷贝构造赋值等是编译器给的拷贝构造赋值方式,一位一位的搬过去。
而带指针的类(string)拷贝构造赋值需要自己写,不能按编译器的来。
1 | class String |
带指针的要写三个特殊函数(Big Three):
- 拷贝构造函数
- 拷贝赋值
- 析构函数
ctor和dtor(构造函数和析构函数)
1 | inline |
带指针的要写析构函数,因为动态分配内存需要释放,对象死亡的前一刻,要用delete将内存杀掉。
拷贝构造和拷贝赋值
如果不自己编写,编译器做一位一位的拷贝。并且会发生 内存泄漏
。
如,字符串赋值,b=a,赋值后,b原先的内存没有指针指向,造成内存泄漏。同时,a和b内存由相同的指针指向,改a或b会对对方造成影响,很危险。这种叫浅拷贝
。
而需要写的是 深拷贝
。
拷贝构造函数
1 | inline |
1 | { |
拷贝赋值函数:
左边=右边。
过程:先把左边清空,分配出和右边一样的空间,再把右边拷贝过来。
1 | inline |
栈(stack),堆(heap)
本节讲new
Stack,是存在于某作用域(scope) 的一块内存空间(memory space)。当你调用函数,函数本身即会形成一个stack 用来放置它所接收的参数,以及返
回地址。
在函数本体(function body) 内声明的任何变量,其所使用的内存块都取自上述stack。
Heap,或者称为system heap,是指由操作系统提供的一块global 内存空间,程序可动态分配(dynamic allocated) 从某中获得若干区块(blocks)。
1 | class Complex { … }; |
-
c1 便是stack object,其生命在作用域(scope) 结束之后结束。这种作用域内的object,又称为auto object,因为它会被「自动」清理。
-
c2 便是static object,其生命在作用域(scope)结束之后仍然存在,直到整个程序结束。
-
还有一种是全局对象(global object),c3 便是global object,其生命在整个程序结束之后
才结束。你也可以把它视为一种static object,其作用域是「整个程序」。 -
p 所指的便是heap object,其生命在它被deleted 之后结束。
如果不加delete,会出現内存泄漏(memory leak),因为当作用域结束,p 所指的heap object 仍然存在,但指针p 的生命却结束了,作用域之外再也看不到p(也就沒机会delete p)。
C++中分配内存和释放的内部实现是C语言中的malloc,free。
new: 先分配memory,再调用ctor(Complex)
delete:先调用dtor,再释放memory(Complex)
new: 先分配memory,再调用ctor(String)
delete:先调用dtor,再释放memory(String)
动态分配内存块(memory block), in VC
动态分配所得的array
老师讲的很好,看视频。其他教程和书籍一般不讲这些。
概括来说,就是操作系统中实际分配的内存并不是我们平时认为的大小,而是有一些附加内存,并且有调试模式和非调试模式。分配的内存大小要是16的整数倍。小于的话要补上pad(00000000)。
array new 一定要搭配array delete
delete[]:表示删除的是数组,编译器会调用多次析构函数。