Loading

候捷-C++面向对象高级开发

笔记参考

一些候捷C++视频比较完善的学习笔记,可以参考学习一下:

学习目标

  • 培养正规的、大气的编程习惯
  • 以良好的方式编写C++ class (没有指针成员——complex.h示例、有指针成员——string.h示例)——基于对象
  • 学习Class之间的关系继承、复合、委托(opp-demo.h示例)——面向对象

complex类

构造函数

使用构造函数的初值列初始化类的成员变量,参考C++构造函数初始化列表与赋值
适当的做法:

//构造函数
complex(double r = 0, double i = 0)
   :re(r),im(i)//初值列,初始列
{ }

不适当的做法:

//构造函数
complex(double r = 0, double i = 0)   
{re=r;im=i;}//赋值

注:不带指针的类大部分不需要析构函数

常量成员函数

在类的成员函数说明后面可以加const关键字,则该成员函数为常量成员函数。常量对象,以及常量对象的引用或指针都只能调用常量成员函数,参考 C++之常量成员函数
成员函数在不修改变量的情况下尽量加上const

double real() const {return re;}
double imag() const {return im;}

参数传递

成员函数中参数传递尽量使用引用,参数传递方式主要有以下几种:

  • 值传递:不推荐,只有参数内存大小在4个字节以内才会使用,参数传递语法如 (Complex add)
  • 引用传递:推荐,相对于传指针但比指针更优雅,参数传递语法如 (Complex& add)
  • 常量引用传递:根据需求使用,加了const后引用参数的值不能被函数修改否则编译器会报错,参数传递语法如 (const Complex& add)

函数返回值

成员函数的返回值也尽量使用引用(建立在非局部变量的情况下),函数内申明的局部变量不能使用引用返回。

inline Complex& _doapl(Complex* ths, const Complex& add)
{
    ths->re += add.re;
    ths->im += add.im;
    return *ths;
}

注:使用引用返回值时,传递者无需知道接受者是以引用还是值的方式接受

临时对象

参考 二十一、C++中的临时对象:

  • 直接调用构造函数将产生一个临时对象
  • 临时对象的生命周期只有一条语句的时间(过了这条 C++ 语句,临时对象将被析构而不复存在)
  • 临时对象的作用域只在一条语句中
//typename()创建一个临时对象
inline  Complex operator + (const Complex& x, const Complex& y)
{
    return Complex(x.real() + y.real(), x.imag() + y.imag());
}

友元

在类中指定的友元就可以访问该类中受保护的内容,成员函数、类、全局函数都可以做为友元,参考 C++ 友元
需要注意,相同class的各个objects互为friends(友元)

string类

三大函数

拷贝构造函数(copy ctor):若自定义的类中有指针则要自己定义拷贝构造函,系统默认的构造函数只会浅拷贝(将指针拷贝)。

//拷贝构造函数
inline
String::String(const String& str)
{
    m_data=new char[ strlen(str.m_data) + 1 ];
    strcpy(m_data, str.m_data);
}

String s1("hello");  
String s2(s1); //直接取另一个object的private data(兄弟之间互为friend)

拷贝赋值函数(copy assignment operator):将原来的空间清空,分配一个和拷贝的对象的一样大的空间,将内容拷贝过去。

//拷贝赋值函数
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;
}

String s2 = s1;

析构函数:析构函数会在此类对象被释放时自动执行,如离开作用域时。

inline
String::~String()  //离开作用域,调用析构函数
{
    delete[] m_data;  //释放掉因创建对象动态分配的内存
}

带指针的类如果使用默认的拷贝构造函数、拷贝赋值函数会产生内存泄漏、别名的严重错误,如下图所示:

堆、栈与内存管理

栈(Stack):存在与某作用域(scope)的一块内存空间(memory space),如当调用函数时,函数本身即会形成一个stack用来放置它所接收的参数,以及返回地址。
在函数本体(function body)内声明的任何变量,其所使用的内存块都取自上述stack,离开作用域的时候会自动释放。

堆(Heap):由操作系统提供的一块 global 内存空间,程序可动态分配(dynamic allocated)从其中获得若干区块(blocks)。
当离开作用域{}的时候,动态分配的内存不会消失,即从堆中动态取得的内存不会自动消失,需要手动释放(delete 掉)。

静态对象:一个对象前面加上 static 修饰符后,既变成所谓的静态对象(static object),其生命在作用域(scope)结束之后仍然存在,直到整个程序的结束。

全局对象:定义在任何作用域或者说大括号之外的对象,其生命在整个程序结束之后才结束,也可以把它视为一种static object,其作用域是整个程序。

new这个动作被编译器分解为分配内存转换类型调用构造函数三个动作:

Complex *pc;
void* mem = operator new( sizeof(Complex) );  //分配内存
pc = static_cast<Complex*>(mem);	//强制类型转换
pc->Complex::Complex(1,2);		//构造函数

与new对应的是delete,编译器解释为调用析构函数释放内存两个动作:

Complex::~Complex(pc);		//析构函数
operator delete(pc);		//释放内存

如果对动态分配所得的内存块(in VC)感兴趣,可以参考堆、栈与内存管理

扩展补充:类模板、函数模板及其他

关于类方数据成员、静态成员、函数、静态函数:

  • 类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。修改一个对象的数据成员,不会影响其他本类的对象。
  • 类的静态数据只有一份,静态数据在类内部声明后需要在类外部定义
  • 成员函数只有一个,但它要处理很多个对象,this会传递进成员函数,this(本质为指针)告诉成员函数在什么时候该处理哪一个对象。
  • 静态函数与非静态函数的差别在于静态函数没有this,只能处理静态数据。

如果函数中存在静态变量,只有函数被调用时函数中的静态变量才会开辟地址,如懒汉式单例模式如下:

class A {
	public:
	static A& getInstance();//getInstance()是对外的唯一窗口
	setup(){ ... }
	private:
	A();
	A(const A& ths);
	...         //相比singleton,这里的static A a;放到了下面的getInstance()中
};

A& A::getInstance()
{	//如果没有人使用getInstance()那a就不存在
	static A a;//只有调用getInstance()对象a才会被创建
	return a;//离开这个函数对象a还存在并未死亡
}

一个关于类模板(class template)的示例:

template<typename T>//目前T还没有绑定具体数据类型,T只是个符号
class complex
{
	public:
		complex (T r = 0, T i = 0) : re(r),im(i)
		{ ... }
		complex& operator += (const complex&);
		T real () cosnt { return re; }
		T imag () cosnt { return im; }
	private:
		T re, im;
		friend complex& _doapl(complex*, const complex&);
};//谨记勿忘分号

{
	complex<double> c1(2.5, 1.5);//用double替换上述全部T
	complex<int> c2(2,6);//用int替换上述全部T
	...
}

一个关于函数模板(function template)的示例:

stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);//函数模板不需要像类模板中明确指出绑定类型,如complex<int> c2(2,6);绑定类型为int.函数模板会进行实参推导

template <class T>
inline
const T& min(const T& a, const T& b)
{
	return b < a ? b : a;//编译器不知道如何比较stone b和stone a(要进行操作符重载),设计这个比大小的人,责任不在他身上,而在设计stone的人.
	//在C++中的算法全部都是function template
}

继承、复合、委托

Composition(复合):是has-a关系,表示‘我’里面有另外的东西,在UML图中用组合来描述这种关系。

注:UML用实心的菱形+实线箭头来表示组合(Composition),组合表示部分和整体的关系且生命周期是相同的,如:人与手 。

Delegation(委托):也叫Composition by reference(两个类通过引用(指针)复合),是一种有点虚的has-a关系,指针指向的对象可能随时变换,在UML图中用聚合来描述这种关系。

注:UML用空心的菱形+实线箭头来表示 聚合(Aggregation),聚合表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分,如:公司和员工。

Inheritance(继承):是is-a关系,继承方式public、private、protected有3种,最重要的情况就是public,数据是可以完整的继承下来的。

以上三种情况下构造函数、析构函数都遵循构造由内而外、析构由外而内的顺序执行。

虚函数与多态

虚函数、纯虚函数、非虚函数的区别如下:

  • virtual(虚)函数:希望devired class(子类)最好去重新定义(overide),且对它(指父类的虚函数)已有默认定义。
  • pure virtual(纯虚)函数:希望devired class(子类)一定重新定义(overide),对它(指父类的虚函数)完全没有默认的定义(其实可以由定义,但你不去定义它)。
  • non-virtual函数(不是虚函数):希望drived class(子类)不要重新定义(overide,覆盖)。

虚函数、纯虚函数示例如下:

一个非常经典的设计(通过子类对象调用父类函数):

注:myDoc调用父类的OnFileOpen(),myDoc(谁调用我的那个谁)就会成为隐藏的this pointer。从编译器的角度考虑,它会这样写CDocument::OnFileOpen(&myDoc);&myDoc,即myDoc的地址是隐藏的参数,它将被传到父类中的OnFileOpen()函数的参数中。this->Seirialize();通过this 调用Seirialize(),tthis就是&myDoc

posted @ 2022-09-19 23:14  二次元攻城狮  阅读(304)  评论(0编辑  收藏  举报