华南理工大学读本科的时候,大一大二入门了C++,但是始终不深。现在继续在华工深造,希望继续深入C++。
今天我正式开始继续好好学习C++的历程。将在CSDN持续更新。
先重拾基础。《C++ primer》/《Effective C++》 / 侯接视频。**21年底前完成所有的基础工作。一共两个多月。**C++ primer工具书也再次浏览了一遍,大而广的东西没能够深入,因为没有应用场景,因此这个工具书待到遇到再回来看看。
深入开源项目。Redis/Muduo等。将在下年深入进行,这年只是大概看看。
兼顾好实验室项目以及课程的同时,尽量抽时间完成。加油。
本篇记录的是【侯捷 - C++面向对象高级开发】入门课程。
本课程一共是上下两部分,上部分主要讲基础的OOP思想以及方法,下部分是深入的解析。总课时估计是十来小时,放在四五天学习会挺舒服的。
源自公众号:编程指北
链接: https://pan.baidu.com/s/19REVrk-_3lpQu_fUmRBRUw 密码: 7iup
或有条件也可以直接访问Youtube资源。
将数据和函数方法绑定在一起,方便(方法整合到了一起)、安全且隐私(数据不被观察)。
多个文件引入头部文件的时候,避免重复定义。
攥写方法如下:
complex.h
#ifndef __COMPLEX__ //如果未曾定义,则定义
#define __COMPLEX__
#endif
前置声明、类声明、类定义。
//前置声明,都先声明,让前面的类中找到后面类的符号
class ostream;
class complex;
//类声明
class complex{};
//类定义
complex::function(){
}
希望类中某个属性不直接绑定内置类型,因此直接把类型抽象成一个模板。
//定义
template<typename T>
class complex{
public:
complex(T a,T b):re(a),im(b){}
private:
T re,im;
};
//实例化
complex<double> c2(2.5,1.5)
complex<int> c1(2,6)
如果函数在class内定义完成,则会成为inline函数(但要求函数简单,编译器不拒绝)。
但某些复杂函数无法inline,编译器会拒绝。
public / private
建议所有的数据都放在private。
构造函数的函数名称与类一致。无返回值。
另外,不带指针的类通常不需要写析构函数。
//任何函数都可以写默认实参
//使用初值列,initialization list(只有构造函数才有的语法)
complex(double r = 0,double i = 0):
re(r), im(i){ }
//这种赋值方法比上面不好
//原因是:初始化和赋值分离了,多走了一步,而上面一致
complex(double r = 0,double i = 0)
{ re = r ; im = i; }
重载不仅指入参可以不一样,出参也可以不一样。
用作成员set和用作get的函数名可以一致。
//get
double real() const{return re;}
//set
void real(double r){re = r;}
complex(double r = 0,double i = 0):
re(r), im(i){ }
//等价构造,不行
complex():
re(0), im(0){ }
把构造函数放在private,就不能被公共创造。
class A{
public:
static A& getInstance();
setup(){...}
private:
A(); //默认构造
A(const A& rhs); //赋值copy构造
}
A& A::getInstance(){
static A a;
return a;
}
定义函数的时候,不希望该函数有修改成员值的权力,就加const限定词。
double real() const{ reutrn re; }
尽量传ref。
传value是复制了一份数据到栈上。
传ref是直接传引用(引用的地步就是指针,四个字节)。
所以传ref快,而且方便改值(尽管传char可能更小,一个字节,但是没必要这么细,通常来说传ref快)。当然如果希望不被改值,加const关键词。
// pass by ref to const,看入参
complex& operator += (const complex&);
尽量返ref。
通常封装数据之后,外部都无法访问数据。**但是友元可以。**事实上友元有点违反封装的设计原理。
class complex{
private:
//声明,其实这个是标准库的代码,名字也是标准库团队写的,do assignment plus
friend complex& __doapl(complex*,const complex& r);
}
//定义
inline complex&
__doapl(complex* ths, const complex& r){
ths->re += r.re; //此处就直接访问了private的成员变量
this->im += r.im;
return *ths; //此处返回一个object
}
相同class的各个objects互为friends。即同类的objects可以互相访问private data。
因此需要在类中写好定义。
使用场景
complex c1;
complex c2;
c1 += c2;
//或者连串赋值,从右往左叠着叠加
complex c3;
c3 += c2 += c1;
则实现方式:
inline complex& complex::operator += (const complex& r){
return __doapl(this,r);
}
这里隐藏了操作符的左操作数,是this,参数列表中不需要显式写出,可以直接使用。
使用场景
complex c1;
complex c2;
//以下用法都可能,很多
c2 = c1 + c2;
c2 = c1 + 5;
c2 = 7 + c1;
则实现方式:
这个绝对不可以返回reference,因为相加的object一定是新建且是local的。因此需要创建临时对象(无名称,只在当前行有用)返回。临时对象的内存地址与return出去后的内存地址一致吗?
使用场景
complex c1;
+c1;
-c1;
实现方法
inline complex&
operator +(const complex& x){
return x;
}
inline complex&
operator -(const complex& x){
return complex(-real(x),-imag(x));
}
使用场景
complex c1;
complex c2;
c1 == c2;
c1 == 0;
0 == c1;
使用场景
complex c1(2,1);
cout << c1;
cout << c1 << c1; //为了保证能够连串输出,所以得返回ostream&(当然不返回ref也可以,但是效率低)
实现方案
#include
ostream&
operator << (ostream& os, const compelex& x){
return os << '(' << real(x) <<',' << imag(x) << ')';
}
以上是经典案例1,complex,class without pointer member
以上是经典案例2,string,class with pointer member
场景
String s1();
String s2("我是一个酒精过敏的帅哥");
String s3(s1); //拷贝构造
s3 = s2; //拷贝复制
**如果成员函数含有指针,必须含有copy ctor和copy op=。*否则如果使用默认的方法,只是潜赋值,会共用同一个底层的char。
class String {
public:
String (const char* cstr = 0); //构造函数
String (const String& str); //拷贝构造
String& operator=(const String& s); //拷贝赋值
~String(); //析构函数
char* get_c_str() const(return m_data); //get方法,声明为常量成员函数
private:
char* m_data;
}
inline
String::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';
}
}
inline
String::~String(){
delete[] m_data;
}
inline
String::String(const String& str){
m_data = new char[str(str.m_data) + 1];
strcpy(m_data,str.m_data); //之所以可以访问private data,是因为同class的objects是友元
}
inline
String& String::operator=(const String& str){
if (this == &str){ //检测自我赋值,self assignment。不然下面delete后无法赋值
return *this;
}
delete[] m_data; //回收内存
m_data = new char[ strlen(str.m_data) + 1]; //重新分配
strcpy(m_data,str.m_data); //copy值过去
return *this;
}
{
Complex c1(1,2); //栈,local object
Complex* p = new Complex(3); //堆
}
其生命在作用域结束后仍然存在
{
static Complex c;
}
其生命一直存在,直到程序结束
Complex c3; //global objects
int main{
}
存在,直到被delete掉。如果不delete则内存泄漏(即没有回收,且没有办法再手动回收)。
{
Complex* p = new Complex;
delete p;
}
全局空间会有一份类中的方法、静态成员变量、静态成员函数。而正常的成员变量会在初始化object的时候创建。
普通函数处理普通数据(需要传this pointer),当然也可以处理静态数据。
静态函数不能处理普通数据,只能处理静态数据。
静态数据除了在类里面做好生命,必须要在类外做好定义。
Singleton中用到了静态变量(唯一的实例)、静态函数(get方法)。
class Account{
publid:
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); //调用方法1:直接访问
Account().set_rate(5); //调用方法2:通过实例访问
}
cout继承于ostream,标准库团队在ostream实现重载了大量类型的<<操作符。
使用场景:任意元素的比较大小,使用函数模板
//调用函数的时候不必显式说明类型,编译器会推导
template <class T>
inline
const T& min(const T&a,const T&b ){
return b<a ? b:a;
}
当然如果自己设计的类要调用该函数,一定是需要自己重载<操作符。
class A{
public:
bool operator < (const stone& rhs) cosnt{
return this.___< ths.___;
}
}
namespace是一个区域限制,相当于局部空间。可以如下使用:
using namespace directive
using std::cout;
cout<<"hello";
std::cout<<"hello";
面向对象的三个重要特性:复合、委托、继承。
一个类中,包含了另一个类,就叫复合。生命周期同步。
如下,queue类里面包含了Sequence类。
template<class T, class Sequence = deque<T>>
class queue{
protected:
Sequence c;
}
假如Container has a Component.
一个类,仍然包含另一个类,但是不是通过内存直接包含,而是用一个指针包含。 生命周期不同步。
如下,String类委托了一个StringRep类。
class StringRep;//前置声明
class String{
private:
StringRep* rep; //委托
}
委托可以对外接口一致,String接口永远不变,但是内部实现可以通过修改StringRep改变。
类A是类B,则类A继承类B。这个关系清晰易懂,用显示情况get
class Shape{
public:
virtual void draw() const = 0; //纯虚函数,pure vitual
vitual void error(const std::string& msg); //非纯虚函数, impure virtual
int objectID() const; //non-virtual
}
课程说了一个经典案例,多个Obsever观察同一个数据/文档。每个Observer都有自己对数据的显示方式。
代码如下:
class Subject{
int m_value; //要被观察的数据
vector<Observer*> m_views; //观察者列表
public:
void attach(Observer* obs){
m_views.push_back(obs);
}
//修改值之后,要通知所有的Observer,让它们做出改变
void set_val(int value){
m_value = value;
notify();
}
void notify(){
for(int i=0;i<m_views.size();++i)
m_views[i]->update(this,m_value); //调用Obeserver的方法,去更新Obeserver的显示。把自己指针传出去
}
}
class Observer{
public:
virtual void update(Subject* sub,int value) = 0; //纯虚函数,让子类的观察者真正去显示这个值
}
尚未感受到有多厉害,截个图把。
父类想创建未来才定义的子类。同样还没感受到,截个图然后教程继续。
后面是“面向对象程序设计”的续集。会讨论:
即讲述以下技术点
使用场景:我自定义一个类,表示分数,但是我希望它做加减乘除的时候,自动转为double。
class Fraction{
public:
...
operator double() const{ //常量函数,该常量就常量. 转换成任何类型都可以
return (double)(分子/分母);
}
}
//使用
Fraction my_frac(3,5);
double d = 4 + my_frac;
class Fraction{
public:
Fraction(int num,int den=1):分子(num),分母(den){};
Fraction operator+(const Fraction& f){
return Fraction(...)
}
}
我们的使用场景仍然和上面一样,希望Fraction d = 4 + my_frac;
,只需要把4隐式构造成Fraction,然后用操作符+相加起来。
设计模式里面的【代理】,就用到了转换函数。
像指针的类,用起来跟指针一样,但是有更多的机制。
链表的node也是一种pointer-like classes。真挺厉害的。
以下给出node的代码
T& reference operator*()const{
return (*node).data; //用*调用的时候,返回的时候node的data部分,即某个列表元素
}
T* pointer operator->() const{
return &(operator*()); //这里返回某个列表元素的地址
}
list<Foo>::iterator ite;
*ite; //获得一个Foo object
ite->method(); //调用Foo::method;
让一个类像函数被调用,只要实现()操作符即可。
小小的类,用作base unit。
把区域分隔开。用::
访问特定区域。
使用场景:更灵活地构造
pair p;
pairp2(p); //支持这种做法,就必须些成员模板。只能up-cast
同时智能指针也实现了成员模板。
泛化模板覆盖很广,但是我们有时候需要针对某种类型,执行不同的操作。
//泛化
template<class Key>
struct hash{};
//特化
template<>
struct hash<int>{
size_t operator()(int x) cosnt{return x;}
}
这部分跟python的偏函数挺像的,就是对template泛化模板,偏特化某个模板参数 或者 修改范围。
个数上的偏特化
//泛化模板
tempalte<typenamte T, typename Alloc=...>
class vector{
...
};
//个数上的偏特化
tempalte<typename Alloc=...>{
...
}
范围上的偏特化
//泛化模板
temaplte<typename T>
class C{
...
};
//针对指针的偏特化模板
template<typename U>
class C<T*>{
...
};
c<string> obj1; //使用泛化模板
c<string*> obj2; //使用偏特化模板
使用场景:希望自定义一个模板,嵌套另一个模板进行使用。
这里有点深了,先有个印象然后跳过吧。
template class Container
>
class XCls{
private:
Container c;
...
}
//使用
template
using Lst = list>;
Xclsmylst2;
cout<<__cpluscplus;
如果输出201103,则表示是C++11。
使用场景:使某函数接受任何多的参数。
void print(){
}
template
void print(const T& firstArg,const Types&... args){
cout<(377),42);
让编译器自动推导type。
list<string> c;
//手动写,写这个必须是程序员的基本素养
list<string>::iterator ite;
ite = find(c.begin(),c.end(),target);
//auto写
auto ite = find(c.begin(),c.enmd(),target);
新的循环方式,像Python一样。
//临时遍历
for (int i:{2,3,4,7}){
cout<<i<<endl;
}
vector<double> vec;
for(const auto& elem:vec){ //pass-by-reference
cout<<elem<<endl;
}
特性:
int x = 0;
int* p = &x; //取指针
int& r = x; //取引用
int x2 = 5;
r = x2; //覆盖赋值,即x被赋值为5
int& r2 = r; //传递性,r2同样引用x
void f2(Cls obj){obj.xxx();}
void f3(Cls& objs){obj.xxx();} //注意是不同函数,如果是同一个函数就会报错,编译器二义性错误
f2(obj);//f2和f3的调用参数一致,不像指针,入参需要设置为&obj
f3(obj);
另外,函数的const是否属于函数签名一部分呢?是的。
高清 1080P C++面向对象高级编程(侯捷) P30 17 关于vptr和vtbl
只要类中有虚函数,就有vptr,它指向vtbl,表里面放的都是函数指针,指向内存里面的虚函数。
这幅图画得非常非常清晰。
这个也就是实现多态的底层原理。
静态绑定直接访问内存里编译好的函数内存空间。
动态绑定调用三个条件:
假如A是B的父类,B是C的父类。
//静态绑定
B b;
A a= (A)b; //向上转型
a.vfunc1(); //调用父类的虚函数,这是静态绑定,汇编形式:call xxx
//动态绑定
A* pa = new B; //向上转型
pa->vfunc1(); //调用父类的虚函数,动态绑定
这里Cpp primer这本书说得贼全,但是很复杂。侯老师说得比较精炼。
当成员函数的const和non-const版本同时存在,
const object 智能调用const版本。
non-const object只能调用non-const版本。
const object (data member不得改动) |
non-const object (data members可改动) |
|
---|---|---|
const member functions (保证不改变data members) |
允许 | 允许 |
non-const member functions (不保证 dat members不变) |
不允许 | 允许 |
即常量对象无法调用非常量成员函数。
当重载[]操作符的时候,设计两个函数,
恭喜完成了【侯捷 C++ 面向对象高级开发】的学习!
本课程一共是上下两部分,上部分主要讲基础的OOP思想以及方法,下部分是深入的解析。总课时估计是十来小时,放在四五天学习会挺舒服的。
Cpp是一个庞然大物,不必期待那么快就能学完。
后面我将继续进行侯接课程的学习,持续更新。
欢迎评论交流!