C++ Part1 面向对象编程合集
1 头文件与类的声明
1.1 c vs cpp关于数据和函数
c语言中,data和函数都是分别定义,根据类型创建的。这样创建出的变量,是全局的
cpp中,将数据data和函数都包含在一起(class),创建出一个对象,即为面向对象;数据和函数(类的方法)都是局部的,不是全局的
class的两个经典分类:
- 无指针成员的类(complex)——复数
- 有指针成员的类(string)——字符串
1.2 头文件与类
1.2.1 头文件
引用自己写的头文件,用双引号
头文件的标准写法:
complex.h:
#ifndef _COMPLEX_ // 如果没有被定义过就定义 (防卫式声明)
#define _COMPLEX_
#endif
- 首先是防卫式声明,如果没定义这个名词,那么就定义一下。ifndef+define。(这样如果程序是第一次引用它,则定义,后续则不需要重复定义,不需要重复进入下面的过程)
1
要写的类的声明,2
是要写类的具体定义,写1
2
的时候发现有一些东西需要提前声明,写在0
处
1.2.2 class的声明
在C++中 struct和class唯一的区别就在于默认的访问权限不同
- struct 默认权限为公共
- class 默认权限为私有
class complex //class head
{ //class body /*有些函数直接在此定义,另一些在 body 之外定义*/
public:
complex (double r = 0, double i = 0)
: re (r), im (i)
{ }
complex& operator += (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex*, const complex&);
};
{
complex c1(2,1);
complex c2;
...
}
1.2.3 模板初识
{
complex<double> c1(2.5, 1.5);
complex<int> c2(2, 6);
...
}
- 因为实部和虚部的类型不确定,可能是
double
float
int
,定义一个模板类型叫做T
- 将
T
作为一个类型参数来传入,在调用的时候就可以指定类型了 - 通过在定义类的前面加入一行代码
template<typename T>
来实现
2 构造函数
2.1 inline 函数
定义类的时候,可以直接在body中定义函数(inline函数,在body中定义完成),也可以只是在body中声明函数
- inline内联函数:如果定义的函数是内联函数,那么会运行比较快,尽可能定义为内联函数
- 在body外,通过
inline
关键字来指定该函数为inline函数。
注意的是,上面所有的inline函数,都只是我们指定的,希望它为inline,具体是不是,要看编译器来决定
2.2 访问级别
数据应该被定义为private
函数要被外界使用,定义为public;若只是内部处理,定义为private
2.3 ctor 构造函数
2.3.1 ctor 的写法
方式一:(推荐)
complex(T r = 0, T i = 0) //函数名称与class的名称一致
: re(r), im(i) //中间这一行就是初始化
{ }
方式二:(不推荐)
complex(double r = 0, double i = 0)
{
re = r; im = i; //用赋值来进行初始化
}
通过构造函数来创建对象。会自动调用构造函数进行创建。
- 构造函数名称需要与类的名称一样
- 函数的参数可以有默认参数
- 构造函数没有返回类型
2.3.2 ctor/函数 重载
构造函数可以有很多个,可以重载;但是上面的1
2
两个构造函数冲突了
complex c2(); // "()" 可以不要,一样的
上面的调用方式对两个构造函数都适用,冲突
double real () const { return re; }
void real (double r) { re = r; } //不能有const
- 同名的函数可以有多个,编译器会编成不同的名称,实际调用哪个会根据哪个适用
2.3.3 ctor 放在 private 区
- 通常构造函数不要放在private中,这样外界没法调用,也就无法创建对象
- 在设计模式
Singleton
(单体)中,将构造函数放在了private中;这个class只有一份,外界想要调用的时候,只能使用定义的getInstance()
函数来取得这一份;外界无法创建新的对象
2.4 const 常量成员函数
对于不会改变数据内容的函数,一定要加上const
{
const complex c1(2, 1);
cout << c1.real();
cout << c1.imag();
}
对于上面调用方式,我们创建一个常量复数然后调用函数输出实部虚部,如果上面real和imag函数定义的时候,没有加const,那么这里函数默认的意思是可能会改变数据,与我们的常量复数就矛盾了,编译器会报错;因此,对于不会改变数据内容的函数,一定一定要加const
3 参数传递与返回值——引用
3.1 参数传递
值传递 pass by value,传递value是把整个参数全传过去,尽量不要直接value传递 例
double r
引用传递 pass by reference,传引用相当于传指针,快,形式也漂亮 例
complex&
如果只是为了提升速度,不向改变数据,那么传const引用;这样传进去的东西,不能被修改
例
const complex&
3.2 返回值传递
返回值的传递,尽量返回引用
在函数中创建的变量 (local 变量),要返回——这种情况是不能返回引用的;因为函数结束后函数中创建的变量就消失了,无法引用
传递者无需知道接受者是以reference形式接受——所以用reference形式很便捷
4 友元 friend
4.1 友元
友元:friend,修饰在函数定义之前,表示这个函数可以直接拿该类对象的private数据
inline complex&
__doapl(complex* ths, const complex& r)
{
ths->re += r.re; //直接拿private的数据,不需要函数
ths->im += r.im;
return *ths;
}
- 如上面所示,声明为friend之后,函数可以直接取到re和im,如果不被声明为friend,只能通过调用real和imag函数来得到,效率较低
4.2 相同 class 的 object 互为 friends
{
complex c1(2, 1);
complex c2;
c2.func(c1);
}
相同class的不同对象互为友元,即可以直接取另一個 object 的 private data
5 操作符重载与临时对象
5.1 操作符重载
在c++里我们可以定义加法等操作符,比如我们可以定义两个石头的加法
5.1.1 成员函数实现 / this
成员函数: complex :: function ....
前面带有class的名称(在class里先声明了的)
inline complex&
complex::operator += (const complex& r) {
return __doapl(this, r); //do assignment plus
}
所有的成员函数都带有一个隐藏的参数this
(是一个指针),this
指向调用这个函数的调用者
定义函数的时候,在参数中不能写出来
this
,直接用即可函数里可写可不写,但当传入参数与成员变量名相同时要写
public: double real () const { return this->re; } //这里的this->可省略
c3 += c2 += c1; // c2 加了 c1 后如果返回 void 就无法进行 c3 的操作了
将操作符写为void函数也可以,但为了可以兼容c3+=c2+=c1
的形式,写成返回引用更好。
5.1.2 非成员函数实现
非成员函数没有this
应对三种使用方法,写出三种方式
非成员函数是global函数——为了后面两种使用方法
这些函数不能返回引用,必须值传递
在函数中创建的新变量 (local 变量),要返回
5.1.3 output函数 << 的重载
cout不认识新定义的这种复数,因此也需要对<<
进行操作符重载
只能全局函数,不能成员函数——导致使用时方向相反
#include <iostream.h>
ostream&
operator<<(ostream& os, const complex& x)
{
return os << '(' << real(x) << ',' << imag(x) << ')'; //自定义输出
}
ostream&
是cout
的 classname
参数传递:os 在函数中会变化,所以不能加
const
返回值传递:为了避免
cout << c1 << conj(c1);
连续输出,不用void
cout << c1
返回值需要与cout
类型一致
5.2 临时对象
classname ()
创建一个classname类型的临时对象——不需要名称,生命只有一行
6 带指针的类:三大函数
析构函数:
~String();
拷贝构造函数 copy ctor :
String (const String& str);
——string s3(s1)
拷贝赋值函数 copy op= :
String& operator=(const String& str);
——s3=s2
编译器默认的拷贝构造赋值(一个bit一个bit的复制),编译器默认的只是拷贝了指针(浅拷贝),而不是指针指向的数据
alias(别名)和 memory leak(内存泄漏)都是十分危险的
因此,如果类中有指针,一定自己写这两个函数
6.1 ctor 和 dtor (构造和析构函数)
6.1.1 ctor 构造函数
这里的 new
是申请的字符串的空间
inline
String::String(const char* cstr = 0)
{
if (cstr) { // 指定了初值—— String s2("hello");
m_data = new char[strlen(cstr) + 1]; // 字符串长度 + /0
strcpy(m_data, cstr);
}
else { // 未指定初值—— String s1();
m_data = new char[1];
*m_data = '\0';
}
}
这里的 new
是申请的指针的空间,String()
里面还有一个 new
String* p = new String("hello");
delete p;
6.1.2 dtor 析构函数
inline
String::~String()
{
delete[] m_data;
}
每个 new
都对应一个 delete
—— 一定要释放
类对象死亡的时候(离开作用域),析构函数会被自动调用
例:这里结束会调用三次 dtor
{
String s1(),
String s2("hello");
String* p = new String("hello");
delete p;
}
6.2 copy ctor 拷贝构造函数
inline
String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1]; // “str.m_data” 兄弟之间互为友元
strcpy(m_data, str.m_data); // 深拷贝
}
String s1("hello ");
String s2(s1);
6.3 copy op= 拷贝赋值函数
先杀死调用者
重新申请指定大小的空间
复制字符串内容到调用者
inline
String& String::operator=(const String & str)
{
if (this == &str) // 检测自我赋值 self assignment
return *this;
delete[] m_data; // 第一步
m_data = new char[strlen(str.m_data) + 1]; // 第二步
strcpy(m_data, str.m_data); // 第三步
return *this;
}
一定要在开始就检测自我赋值,因为
a=a
时第一步delete
了后,会使第三步出现问题
7 堆,栈,内存管理
7.1 堆和栈
Stack 栈,是存在于某作用域 (scope) 的一块内存空间。
例如当你调用函数,函数本身即会形成一个 stack
用来放置它所接收的参数,以及返回地址;在函数本体 (function body) 内声明的任何变量其所使用的内存块都取自上述 stack
Heap 堆,或称为 system heap ,是指由操作系统提供的一块 global 内存空间,程序可动态分配 (dynamic allocated) 从中获得若干区块 (blocks)
可以用 new
来动态取得
在 stack 中的是自动生成的空间,作用域结束空间会自动释放
在 heap 中的是自己申请的空间,需要自己释放
{
complex c1(1,2);
/*c1空间来自stack*/
complex* p = new complex(3);
/*complex(3) 是个临时对象
其所用的空间是以new从heap动态分配而得,并由p指向*/
}
7.2 object 生命期
stack objects 的生命期
c1
便是所谓 stack object,其生命在作用域 (scope) 结束之际结束这种作用域内的 object,又称为 auto object,因为它会被“自动”清理(结束自动调用析构函数){ complex c1(1,2); }
static local objects 的生命期
若在前面加上
static
后,其会存在到整个程序结束{ static complex c2(1,2); }
global objects 的生命期
写在任何作用域之外的对象,其生命在整个程序结束之后才结束,你也可以把它视为一种 static object,其作用域是整个程序
... complex c3(1,2); int main() { ... }
heap objects 的生命期
p
所指的便是 heap object,其生命在它被delete
之际结束{ complex* p = new complex; ... delete p; }
7.3 new 和delete
7.3.1 new
new:先分配 memory , 再调用 ctor
- 分配内存:先用一个特殊函数,按 class 的定义分配了两个
double
的大小 - 转型(忽视)
- 调用构造函数,赋值
(1,2)
7.3.2 delete
delete:先调用 dtor, 再释放 memory
- 调用析构函数——释放的是
m_date
指向的字符串Hello
的空间(即构造函数中new
申请的空间) - 释放内存:用一个特殊函数释放了
ps
指向的空间(即String* ps = new String("Hello");
中new
申请的空间)
7.4 内存动态分配
7.4.1 在VC下内存动态分配
在VC下(不同编译器的内存动态分配可能不同)
调试模式:
(4*3)
是3个指针的大小(32+4)
是调试模式所需空间(橘色部分)(4*2)
是上下两个 cookie ——表示内存块的开始与结束4
是数组才有的长度记录由于分配内存块需要是16的倍数,所以需要 pad 来填充到
64
执行模式:
去掉调试模式的空间即可
因为内存块是16的倍数,因此最后四位bit一定都是0,cookie 就借用最后的一位
1
表示占用内存,0
表示释放内存如上图
41h
中1
即表示占用内存
7.4.2 array new/delete
array new
一定要搭配 array delete
new
后有[ ]
—> delete
后加[ ]
普通的delete只调用一次析构函数——剩下两个指针的指向的空间没有调用析构函数,内存泄漏
这种情况发生在有指针的类,但最好都这样写
8 静态 模板 namespace
8.1 static
对于非静态的函数和数据:
非静态的成员函数通过this
指针来处理不同的数据(一份函数--->多个对象)
对于静态的函数和数据:
静态函数没有this
,不能处理一般的数据,只能处理静态的数据
例1:
class Account
{
public:
static double m_rate; //静态变量的声明
static void set_rate(const double& x) { m_rate = x; } //静态函数
};
double Account::m_rate = 0; //静态变量的定义 一定要有
int main()
{
//调用静态函数法1——by class name
Account::set_rate(5.0);
//调用静态函数法2——by object
Account a;
a.set_rate(7.0); //静态函数与a无关/无this
}
例2:设计模式 Singleton
(单体)
- 构造函数放在private中,外界无法调用
- 设计了
getInstance
静态函数,来生成并返回唯一的一份
8.2 template
8.2.1 class template 类模板
T
来代替某种类型- 使用时
classname<type1> xxx
,编译器会把T
全部替换为type1
8.2.2 function template 函数模板
比较函数——任何类型都可以进行比较;T
来代替某种类型
应用时,不需要写某种类型——编译器自己会推导
8.3 namespace
对东西进行一个包装(不一定要一次性全写在一起,可分开包装在一起)
namespace name
{
...
}
用法一:using directive
#include <iostream> using namespace std; //直接把包装全打开 int main() { cin << ...; cout << ...; return 0; }
用法二:using declaration
#include <iostream> using std::cout; //只打开一条 int main() { std::cin << ...; //没打开要写全名 cout << ...; return 0; }
用法三:都写全名
#include <iostream> int main() { std::cin << ; std::cout << ...; return 0; }
9 复合 委托
9.1 Composition 复合
类似于c中结构里有结构——class里有class
deque
是一个已经存在的功能很多的类(两头进出的队列);利用deque
的功能来实现queue
的多种操作
该例只是复合的一种情况——设计模式 Adapter
9.1.1 复合下的构造和析构
构造是由内而外
Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己
注意如果要调用 Component 的其他构造函数需要自己写出来
Container::Container(…): Component() { … };
析构是由外而内
Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数
9.2 Delegation 委托
委托就是 Composition by reference;即通过指针把任务委托给另一个类
复合中,内部和外部是一起出现的;而委托是不同步的
这是一个著名的设计模式——pimpl (pointer to implementation) 或者叫 “编译防火墙”
右边怎么变动都不会影响左边
reference counting 多个指针共享一个 “Hello”;但当a要改变内容时, 系统会单独复制一份出来给a来改,b和c依然在共享
10 继承与虚函数
10.1 Inheritance 继承
语法::public base_class_name
public
只是一种继承的方式,还有protect
,private
子类会拥有自己的以及父类的数据
10.1.1 继承下的构造和析构
与复合下的构造和析构相似
构造是由内而外
Container 的构造函数,编译器会自动先调用 Component 的 default 构造函数,再执行自己
注意如果要调用 Component 的其他构造函数需要自己写出来
Derived::Derived(…): Base() { … };
析构是由外而内
Container 的析构函数会先执行自己,之后编译器调用 Component 的析构函数
Derived::~Derived(…){ … /* ~Base() */ };
注意:Base class 的 dtor 必需是 virtual
否则下例会导致结束时只会调用 Base 的 dtor
int main() { Base* ptr = new Derived(); delete ptr; // 只会调用 Base 类的析构函数 return 0; }
10.2 虚函数
pure virtual 函数:
derived class 一定要重新定义 (override 覆写) 它;其没有定义只有声明
语法:
virtual xxxxxx =0;
virtual 函数:
derived class 可以重新定义 (override, 覆写) 它,且它已有默认定义
语法:
virtual xxxxxx;
non-virtual 函数:
不希望 derived class 重新定义 (override, 覆写) 它
10.3 继承 with virtual
例子:在 Windows 平台下用某个软件打开文件——分为好几步,但基本所有软件大多数操作都是一致的,只有一个操作如读取方式是不一样的
- 现有一个框架 Application framework 其写好了所有必要的函数,其中
Serialize()
就是一个 pure virtual 函数 - 使用这个框架写自己软件的打开文件,就继承这个框架,其中就需要自己 override 覆写
Serialize()
这个函数 - 在执行中,执行
myDoc.OnFileOpen();
中到Serialize()
时,是通过this
来指引到自己写的Serialize()
中去的
把关键动作延缓到子类再做,这是一个经典的设计模式——Template Method
10.4 缩略图
复合:
委托:
继承:
类中的元素: 变量名称 : 变量类型(与代码刚好相反
变量下面加下划线 表示
static
前面加一个
-
表示private
前面加一个
#
表示protected
前面加一个
+
表示public
(一般可以省略)
10.5 继承+复合
这种关系下的构造和析构与之前的类似
第一种:
构造由内到外 先 Base 再 Component
Derived 的构造函数首先调用 Base 的 default 构造函数,然后调用 Component 的 default 构造函数,然后才执行自己
Derived::Derived(…): Base(),Component() { … };
析构由外而内 先 Component 再 Base
Derived 的析构函数首先执行自己,然后调用 Component 的析构函数,然后调用 Base 的析构函数
Derived::~Derived(…){… /*~Component() ~Base()*/};
第二种:
同理构造由内到外,析构由外而内
10.6 继承+委托
10.6.1 例一 Observer
设计模式—— Observer
例如一串数据,可以用饼图来观察,也可以用条形图来观察,这种种的观察方式都是继承于 Observer
通过 vector<Observer> m_views;
来进行委托
当数据改变的时候,Observer 也需要更新,即 notify
函数,来将目前所有的观察者更新
10.6.2 例二 Composite
设计模式—— Composite
例如文件系统,文件夹里可以有文件夹(与自己相同的类),也可以有文件,其中文件就是最基本的 Primitive,而文件夹就是复合物 Composite
要达成目的,就可以再设计一个父类 Component ,文件和文件夹就继承于同一父类;
其中 Composite 要用委托到父类的方式 Component*
设计容器和操作——使其 Primitive 和 Composite 都可以适用
//父类 Component
class Component
{
private:
int value;
public:
Component(int val) {value = val;}
virtual void add( Component* ) {} //虚函数
};
//复合物 Composite
class Composite
: public Component
{
vector <Component*> c;
public:
Composite(int val) : Component(val) {}
void add(Component* elem)
{
c.push_back(elem);
}
…
}
//基本类 Primitive
class Primitive
: public Component
{
public:
Primitive(int val): Component(val) {}
};
component中add是虚函数(且是空函数),不能是纯虚函数——Primitive 不会 override add函数(最基本的单位,不能 add 了),而 Composite 需要 override add函数
10.6.3 例三 Prototype
设计模式—— Prototype
框架(父类)要创建未来才会出现的子类——要求子类要创建一个自己当作原型 Prototype 让框架(父类)来找到并创建 FindAndClone
补充:当一个子类继承自父类时,它可以被视为是父类的一种类型,因此可以使用父类的指针或引用来引用子类的对象;
这种用父类的指针或引用来处理子类对象的方式称为——**向上转型 ** Upcasting
父类中,有一个存放原型的数组,有纯虚函数
Image *clone()
,还有两个静态函数Image FindAndClone(imageType);
void addPrototype(Image *image){...}
子类中,创建一个静态的自己
_LAST
,把它放到父类的一个空间中,这样父类就可以找到新创建的子类private 的构造函数
LandSatImage()
中是addPrototype(this); //这里的 this 就是 _LAST
将自己的原型放到了父类中去子类中,准备一个
clone()
函数,父类通过调用找到的相应类型的 clone 函数来创建子类的副本这里的 clone 函数就不能用之前的那个构造函数来创建副本了——其会放到父类中去,所以创建一个新的构造函数
LandSatImage(int)
用传进一个无用参数(随便传个int型数据就好)来进行区分