C++面向对象开发上
培养正规的、大气的编程习惯
0. 面向对象三大特征 —— 封装、继承、多态
封装
- 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承
- 基类(父类)——> 派生类(子类)
多态
- 多态,是以封装和继承为基础,使得消息可以多种形式显示。
0) C++ 多态分类及实现:
- 重载多态(Ad-hoc Polymorphism,编译期):函数、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
The Four Polymorphisms in C++
1) 静态多态(编译期/早绑定) 函数重载
class A
{
public:
void do(int a);
void do(int a, int b);
};
2) 动态多态(运行期期/晚绑定)虚函数
虚函数:
用 virtual 修饰成员函数(含基类的虚构函数),使其成为虚函数.
非虚函数:
- 普通(全局)函数(非类成员函数)
- 静态函数(static)
- 构造函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 内联函数不能是表现多态性时的虚函数,解释见:虚函数(virtual)可以是内联函数(inline)吗?
一、C++编程简介
基础知识
曾学过procedural language (C 语言最佳),知道如何对程序编译、链接、执行。
变量(variables)
类型(types) : int, float, char, struct …
作用域(scope)
循环(loops) : while, for,
流程控制: if-else, switch-case
基于对象分类:
基于对象:一个class的编程 object based
面向对象:几个class的编程 object oriented
class的经典分类:
- class without pointer members ——>e.g: complex 复数
- class with pointer members ——>e.g: string 字符串
class之间的关系:
- 继承inheritance、
- 复合composition
- 委托delegation
C++书籍(STL是标准库的前身)
基础:Language
《C++Primer》
《C++programming Language》
提高:Standard Library
《Effective C++ Third Edition》及中文
《The C++ Standard Library》
《STL源码剖析》
二、头文件与类的声明
//flie XXX.h
#ifndef __complex__
#define __complex__
#program once //编译器宏
// class的布局:
class base; //前置声明
class header{// class header
// class body
...
}
#endif //XXX.h end
// XXX.cpp
#include"XXX.h" //c
#include<cstdio> //C++
#include<iostream>
三、构造函数
(1)inline内联函数:
特征
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
//--------------------------声明-----------------------------------------------
// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);
// 声明2(不加 inline)
int functionName(int first, int second,...);’
//--------------------------定义-----------------------------------------------
// 类内定义并实现,隐式内联
class A {
int doA() { return 0; } // 隐式内联
}
// 类外定义,需要显式内联
class A {
int doA();
int functionName(int first, int second,...);
}
inline int A::doA() { return 0; } // 需要显式内联
inline int functionName(int first, int second,...) {/** ...**/};// 需要显式内联
编译器对 inline
函数的处理步骤
- 将
inline
函数体复制到inline
函数调用点处;- 为所用
inline
函数中的局部变量分配内存空间;- 将
inline
函数的输入参数和返回值映射到调用方法的局部变量空间中;- 如果
inline
函数有多个返回点,将其转变为inline
函数代码块末尾的分支(使用GOTO)
优缺点 主要与宏定义比较
优点
>1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
>2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
>3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
>4. 内联函数在运行时可调试,而宏定义不可以。
缺点
>1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
>2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
>3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
(2)access level 访问级别
public
成员:可以被任意实体访问
private
成员:只允许被本类的成员函数访问
protected
成员:只允许被子类及本类的成员函数访问
private
:数据的部分用尽量用private
public
:函数的部分,大部分用public
(3)构造函数
创建一个对象的时候,构造函数自动被调用,构造函数可设置默认参数,并用冒号设置参数初始化列表:
pair(const T1& a, const T2& b) : first(a), second(b) {}
参数initializition list
和在body
里对参数赋值的区别:
一个是参数初始化;
一个是赋值,是一个执行的过程,多了计算量;
创建一个对象,可以有参数,也可以无参数,也可动态创建:
class complex *p = new complex(4); //动态创建
class定义了多个构造函数,就是重载overloading
(4)friend 友元类和友元函数:
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
- 相同class 的各objects 互friends (友元)
#include <iostream> class A { friend class B; // Friend Class }; class B { public: void showA(A& x) { std::cout << "A::a=" << x.a; } }; //OR class B; class A { public: void showB(B&); }; class B { friend void A::showB(B& x); // Friend function }; void A::showB(B& x) { std::cout << "B::b = " << x.b; }
item23. 宁以 non-member、non-friend 替换 member 函数(可增加封装性、包裹弹性(packaging flexibility)、机能扩充性)
//member
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
void clearEverything(); //调用上述的三个函数
};
//non-member
void clearBrowser (WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
(5)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);
}
// Meyers Singleton
class A {
public:
static A& getInstance();
setup() { ... }
private:
A();
A(const A& rhs);
};
A& A::getInstance()
{
static A a;
return a;
}
// Singleton
class A {
public:
static A& getInstance(){ return a;} //???? 猜测结果
static A& getInstance( return a; ); //???? ppt中
setup() { ... }
private:
A();
A(const A& rhs);
static A a;
};
//call 多线程中不安全.
A::getInstance().setup();
四、参数传递和返回值以及const
- 数据放在
private
里 - 参数用
reference
,是否用const
- 在类的
body
里的函数是否加const
- 构造函数的
initial list
- return by reference,不能为local object.
指针和引用的区别:
- 都是地址的概念;指针是一个实体,指向一块内存,它的内容是所指内存的地址;引用则是某块内存的别名。
- 使用sizeof指针本身大小一般是(4),而引用则是被引用对象的大小;
- 引用不能为空,指针可以为空;指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
- 作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
- 引用没有
const
,指针有const
,const
的指针不可变;
具体指没有int& const a
这种形式,而const int& a
是有的,前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)- 指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
- 指针可以有多级指针(**p),而引用至于一级;
- 指针和引用使用自增(++)运算符的意义不一样;
- 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
- 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
const作用
修饰变量,说明该变量不可以被改变;
修饰指针,分为指向常量的指针和指针常量;
常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
修饰成员函数,说明该成员函数内不能修改成员变量。
//const 使用 // 类 class A { private: const int a; // 常对象成员,只能在初始化列表赋值 public: // 构造函数 A() : a(0) { }; // 初始化列表 A(int x) : a(x) { }; // 初始化列表 // const可用于对重载函数的区分 int getValue(); // 普通成员函数 int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值 // 可以供常对象使用,否则报错,调用有可能被改变值,编译器报错。 }; void function() { // 对象 A b; // 普通对象,可以调用全部成员函数 const A a; // 常对象,只能调用常成员函数、更新常成员变量 const A *p = &a; // 常指针 const A &q = a; // 常引用 // 指针 char greeting[] = "Hello"; char* p1 = greeting; // 指针变量,指向字符数组变量 const char* p2 = greeting; // 指针变量,指向字符数组常量 char* const p3 = greeting; // 常指针,指向字符数组变量 const char* const p4 = greeting; // 常指针,指向字符数组常量 } // 函数 void function1(const int Var); // 传递过来的参数在函数内不可变 void function2(const char* Var); // 参数指针所指内容为常量 void function3(char* const Var); // 参数指针为常指针 void function4(const int& Var); // 引用参数在函数内为常量 // 函数返回值 const int function5(); // 返回一个常数 const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6(); int* const function7(); // 返回一个指向变量的常指针,使用:int* const p = function7();
五、操作符重载与临时对象
1、成员函数带有隐藏的参数this
,谁调用这个函数谁就是 this.临时对象 complex() ->typename();
2、在类外Complex Complex::operator+=(complex &c2) 这个是成员函数 operator+= 的实现,所以需要 Complex:: 具有this指针。例如:
inline complex&
__doapl(complex* ths, const complex& r)
{ //第一參數將會被改動 //第二參數不會被改動
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex&
class complex::operator += (const complex& r) //成员函数 this
{
return __doapl (this, r);
}
//而下面属于运算符重载,不是成员函数的时候,就没有Complex:: 。
inline complex
operator - (const complex& x, double y)
{
return complex (real (x) - y, imag (x));
}
六、复习Complex类的实现过程
七、三大函数:拷贝构造,拷贝赋值,析构
构造函数(可以重载,类内创建private)
拷贝构造函数
拷贝赋值函数
析构函数
常量成员函数 const修饰成员函数,防止常量对象进行调用出错。
class with pointer members 必須有copy ctor 和copy op=
一定要在operator= 中檢查是否self assignment
inline String& String::operator=(const String& str)
{
if (this == &str) //自我赋值检查
return *this;
delete[] m_data; //删除
m_data = new char[ strlen(str.m_data) + 1 ]; //new
strcpy(m_data, str.m_data); //copy
return *this; //链式 返回引用
}
#include <iostream.h>
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
——》类含指针,就需要 拷贝构造、 拷贝赋值函数
浅拷贝——》拷贝构造函数 ,浅拷贝的影响:
1、造成内存泄漏;
2、造成有两个指针 指向同一块内存
深拷贝——》拷贝赋值函数
步骤:delete ;new; strcpy;
class里面有默认的拷贝构造和拷贝赋值函数。如果自己不定义一个拷贝构造函数,在调用拷贝构造函数的时候,就会调用默认的浅拷贝构造函数,就会造成问题,所以一定要自己定义拷贝构造函数——深拷贝。
八、堆,栈与内存管理
1、static local objects的生命周期
- static的生命周期 :object的对象在scope结束以后仍然存在,直到整个程序结束;
- 非static 的生命周期:object的对象在在scope结束以后就结束了。
- global objects的生命周期:对象 objects 生命结束,就是什么时候析构函数被调用。
2、new——》operator new。
new动态创建对象,分三步:
- 先转化为operator new 函数,申请分配内存。
- 做类型转化。
- 调用构造函数
delete ——》operator delete。删除对象,分两步:
- 先调用析构函数,
- 再调用operator delete函数。
3、带中括号[ ]的new[ ]叫做array new,带中括号[ ]的delete[ ] 叫做array delete。
动态分配所得到的数组array:complex *p = new complex[3];
new [] ——》delete[] ——》表示调用几次析构函数
new 字符串 ——》delete 指针delete[n] :array new一定要调用array delete,delete[n]会调用n次析构函数,而delete仅调用一次。
Stack,是存在于某作用域(scope) 的一块内存空间(memory space)。例如当你调用函数,函数本身即会形成一个stack 用来放置它所接收的参数,以及返回地址。在函数本体(function body) 内声明的任何变量,其所使用的内存块都取自上述stack。
Heap,或谓system heap,是指由操作系统提供的一块global 内存空间,程序可动态分配(dynamic allocated) 从某中获得若干区块(blocks)。
{
class Complex* p = new Complex;
...
delete p; //若未删除,内存泄露,再也无法删除了,p退出作用域,作用域外无法看到p。
//编译器实现如下
Complex *pc;
void* mem = operator new( sizeof(Complex) ); //分配内存
pc = static_cast<Complex*>(mem); //转型
pc->Complex::Complex(1,2); //构造函数
Complex::~Complex(pc); // 析構函數
operator delete(pc); // 釋放內存
}
{
String* ps = new String("Hello");
//编译器实现如下
String* ps;
void* mem = operator new( sizeof(String) ); //分配内存
ps = static_cast<String*>(mem); //转型
ps->String::String("Hello"); //构
}
九、复习String类的实现过程
class String
{
public:
String(const char* cstr = 0)
{
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}else { // 未指定初值
m_data = new char[1];
*m_data = '\0';
}
}
String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
String& operator=(const String& str);
~String()
{
delete[] m_data;
}
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
inline String& String::operator=(const String& str)
{
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
十、扩展补充:类模板,函数模板,及其他
//class template,类模板
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&);
};
//函数模板
template <class T>
inline const T& min(const T& a, const T& b)
{
return b < a ? b : a;
}
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;
};
//引数推导的结果,T 为stone,于是调用stone::operator<
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);
(1)static :静态数据 不属于某一个对象,而非静态的是属于一个对象的,这种情况需要设置为静态数据。
静态函数 没有this pointer,而非静态函数有 this pointer,可以用this去取数据,静态函数要处理数据只能处理静态数据。静态数据一定要在class外面 定义。给变量赋值,使获得内存的过程叫定义。
(2)template:类模板 函数模板
(3)namespace:
十 一、组合 has-a 与继承 is-a
由内而外template <class T>
struct Itr {
T* cur;
T* first;
T* last;
T** node;
//Sizeof : 4 * 4
};
template <class T>
class deque {
protected:
Itr<T> start;
Itr<T> finish;
T** map;
unsigned int map_size;
//Sizeof : 16 * 2 + 4 + 4
};
template <class T>
class queue {
protected:
deque<T> c;
//Sizeof : 40
};
//构造顺序: Itr->deque->queue 由内到外
//析构顺序: queue->deque->Itr 由外到内
//queue内存大小:queue的内存大小+deque的内存大小
(2)委托delegation,即composition by reference:在body中声明一个带指针的另一个类 composition by reference 生命时间: classA 用一个指针指向classB,需要的时候才调用classB,而不是一直拥有classB。
//委托delegation
// file String.hpp
class StringRep;
class String {
private:
StringRep* rep; // pimpl
};
// file String.cpp
#include "String.hpp"
namespace {
class StringRep {
friend class String; //friend
StringRep(const char* s);
~StringRep();
int count;
char* rep;
};
}
String::String(){ ... }
(3)继承Inheritance:(三种继承方式:public protected private)is-a,继承主要搭配虚函数来使用函数的继承:指的是继承函数的调用权,子类可以调用父类的函数。
//继承中的 构造函数与析构调用顺序
//构造顺序: 父类->子类 由内到外
//析构顺序: 子类->父类 由外到内
//继承+组合模式 构造函数与析构调用顺序
Derived::Derived(...):Base(),Component() { ... };
Derived::~Derived(...){ ... ~Component(), ~Base() };
十二、虚函数与多态
//虚函数 你期待derived class的行为
non-virtual 函数:不重新定义(override, 覆写它).
virtual 函数:重新定义(override, 覆写) 它,且你对它已有默认定义。
pure virtual 函数:必须重新定义(override 覆写)它,你对它没有默认定义。
(1)虚函数:virtual 纯虚函数:一定要重新定义。
(A)Inheritance + composition下的构造和析构
(B)delegation + Inheritance ——》 功能最强大的一种
实例:Template Method模式
//程序库开发人员
class Library
{
public:
//稳定 template method
void Run()
{
Step1();
Step2();//支持变化 ==> 虚函数的多态调用
Step3();
Step4(); //支持变化 ==> 虚函数的多态调用
Step5();
}
virtual ~Library() {}
protected:
void Step1(){}//稳定
void Step3(){}//稳定
void Step5(){}//稳定
virtual bool Step2() = 0; //变化
virtual void Step4() = 0; //变化
};
//应用程序开发人员
class Application : public Library
{
protected:
virtual bool Step2(){ //... 子类重写实现
return true;
}
virtual void Step4(){ //... 子类重写实现
}
};
int main()
{
Library *pLib = new Application();
pLib->Run();
delete pLib;
}
十三、委托相关设计
The End
ps : 学不动了,哈哈哈哈哈哈。
【1】大纲是Gayhub上热心网友的,进行了部分补充,非常感谢共享 。
因个人水平有限,欢迎大家指导。 yzhu798#gmail.com 。2020.03.07
另:https://github.com/yzhu798/CodingInterviewsNotes 有一些其他大佬们的笔记,偷偷整理了下,大家可以去看看,狗头保命。