C++学习6-面向对象编程基础(运算符重载、类的派生与继承、命名空间)

运算符重载

重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。重载的运算符是遵循函数重载的选择原则,根据不同类型或不同参数来选择不同的重载运算符。

运算符重载的基本语法

  • 成员函数形式
<类名> operator<符号>(<参数表>)

参数表列出该运算符需要的操作数。

运算符函数体对重载的运算符的含义做出新的解释。这种解释仅局限在重载该运算符的类中,即当在X类对象的关联中,运算符含义由函数体解释;否则脱离类对象,该运算符具有系统预定义的含义;

  • 友元函数形式
friend <类名> operator<符号>(<参数表>)
选择成员或者非成员函数

当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。下面的准则有助于我们在将运算符定义为成员函数还是普通的非成员函数做出抉择;

  • 赋值(=)、下标([])、调用(()) 和成员访问箭头(->)运算符必须是成员。

对于特殊符号如 =, ->, [], () 在使用运算符重载时,因为当编译器发现当类中没有定义这4个运算符的重载成员函数时,就会自己加入默认的运算符重载成员函数。影响了重载的结果,且还需要注意浅拷贝与深拷贝的问题。

  • 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
  • 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员;
  • 只能用友元、不用成员函数的情况
  • 具有对称性的运算符可能转换任意一端的运算对象,例如输入输出流>>、<<等符号,只能用友元函数重载。

例如:如果是重载双目操作符(即为类的成员函数),如果运算符重载为成员函数,第一个参数必须是本类的对象。
我们使用>> 或<< 时,而cin与cout是系统默认的关键字而非对象名,而左侧运算量又必须是对象名。所以作为成员函数引用时是以下这个样子的:

Iamclassobj << cout;

可这样的用法并不符合常规的应用习惯。

使用友元函数时可以不强制要求左侧运算量必须是对象本身,为了更好的符合代码书写系统,不改变默认符号的原本意义,所以通常使用友元函数对输入、输出流符号进行运算符重载;

详细代码见以下
【特殊运算符重载(输入输出运算符 )】

使用区别

重载为成员函数时,会隐含一个this指针;

当重载为友元函数时,将不存在隐含的this指针,需要在参数列表中显式地添加操作数;

不能运算符重载的符号
> 作用域运算符 ::

> 条件运算符 ?:

> 直接成员访问运算符 .

> 取大小运算符 sizeof

> 指针分量运算符 ->

成员/友元函数形式的运算符重载引用形式

我们除了将运算符作用于类型正常的实参,隐式调用重载运算符函数外。还可以显示调用,像调用普通函数一样调用运算符函数,先指定函数名字,然后传入实参;

对比格式如下:

Tables 成员函数形式 友元函数形式
一元运算 隐式:对象# 隐式:对象#
显式:对象.operator#() 显式:operator#(对象)
二元运算 隐式:对象A#对象B 隐式:对象A#对象B
显式:对象A.operator#(对象B) 显式:operator#(对象A,对象B)

运算符重载的基本语法(成员函数 - 单目运算符)

下面的代码以 前置++ 与 后置++ 的运算符重载语法为例编写;

  • 前置++运算符:前增量操作数与返回值是同一个变量。要求返回的参数是该对象参数的引用,也就是指向操作数的this指针;
  • 后置++运算符:后置操作符不要求返回值为引用,为传递过来的形参定义了一个类类型的临时变量,将this指针赋值给临时变量,对this指针指向的数值增加,然后返回临时变量的值,这里不需要引用

实例代码:成员函数-单目运算符

class CTest
{
public:

	CTest(int val) { m_nNumber = val; };
	//前置++
	CTest & operator++()
	{
		m_nNumber++;
		return *this;
	}

	//后置++
	CTest operator++(int) 
	{
		CTest temp = *this;
		m_nNumber++;
		return temp;
	}


	~CTest() {};
private:
	int m_nNumber;
};

int main()
{
	CTest objNumber(100);
	CTest re  = objNumber++;
	CTest re2 = ++objNumber;
	CTest re3 = objNumber;
	return 0;
}

成员函数-双目运算符

class CNumber {
public:
	CNumber(int val) { m_nValue = val; }
	CNumber operator+(CNumber & op2) {
		CNumber temp = *this;
		temp.m_nValue = m_nValue + op2.m_nValue;
		return temp;
	}
private:
	int m_nValue;
};
int main()
{
	CNumber cNumA(100), cNumB(200);
	CNumber cNumC = cNumA + cNumB;
	return 0;
}

友元函数-单目运算符

使用友元函数进行运算符重载,需要在类内使用friend关键字声明运算符重载函数为友元函数。
然后定义运算符重载函数时,在参数列表内定义类类型的形参。

class CNumber {
public:
	CNumber(int val) { m_nValue = val; }
	friend CNumber & operator++(CNumber & op1); //前置++
	friend CNumber operator++(CNumber &op1, int);//后置++
private:	int m_nValue;
};
CNumber & operator++(CNumber & op1) {
	op1.m_nValue = op1.m_nValue + 1;
	return op1;
}
CNumber operator++(CNumber & op1, int) {
	CNumber temp = op1;
	op1.m_nValue = op1.m_nValue + 1;
	return temp;
}
int main() {
	CNumber cNumA(100);
	CNumber cNumB = cNumA++;
	CNumber cNumD = ++cNumA;
	//CNumber cNumC = cNumA.operator++();
	return 0;
}

友元函数-双目运算符

class CNumber {
public:
	CNumber(int val) { m_nValue = val; }
	friend CNumber operator+(CNumber & op1, CNumber & op2);
private:
	int m_nValue;
};

CNumber operator+(CNumber & op1, CNumber & op2)
{
	CNumber temp(0);
	temp.m_nValue = op1.m_nValue + op2.m_nValue;
	return temp;
}

int main()
{
	CNumber cNumA(100), cNumB(200);
	CNumber cNumC = cNumA + cNumB;
	return 0;
}

特殊运算符重载(赋值运算符 [ ] )

文中使用了strdup函数,strdup函数返回指向被复制的字符串的指针,所需空间由malloc()函数分配且可以由 free() 函数释放。stdrup可以直接把要复制的内容复制给没有初始化的指针,因为它会自动分配空间给目的指针;

#include "stdafx.h"
#include <string.h>
#include <malloc.h>
#include <iostream>
using namespace std;

class MyString {
public:
	MyString(const char * buf) {
		m_szBuf = _strdup(buf);
	}
	~MyString() {
		free(m_szBuf);  //不要忘记释放动态分配出来的内存
	}

	char operator[](int pos)
	{
		return m_szBuf[pos];
	}
private:
	char * m_szBuf;
};
int main()
{
	MyString str("hello world");
	cout << str[8];
	return 0;
}

特殊运算符重载(赋值运算符 = )

以下代码中第33行,str2是调用赋值运算符的对象,str1是参数,34行转换构造+赋值运算符,str2是调用函数,而字符串被转换构造后进入赋值运算符中。

示例代码

#include "stdafx.h"
#include <string.h>
#include <malloc.h>


class MyString {
public:
	MyString(const char*buf) { 
		m_szBuf = _strdup(buf); 
	}
	~MyString() { 
		free(m_szBuf); 
	}

	MyString & operator=(const MyString & str)
	{
		free(m_szBuf);
		m_szBuf = _strdup(str.m_szBuf);
		return *this;
	}
private:
	char * m_szBuf;             //注意浅拷贝问题!!
};
int main() {
	MyString str1("今晚打老虎"), str2("test");
	str2 = str1;               //str2是调用赋值运算符的对象,str1是参数
	str2 = "dddd";             //转换构造+赋值运算符,str2是调用函数,而字符串被转换构造后进入赋值运算符中
	MyString str3 = "ffff";    //转换构造
	//MyString str4 = str1;    //拷贝构造(默认转换构造是浅拷贝,所以释放时会出错)
	return 0;
}

特殊运算符重载(类型转换运算符 )

operator关键字除了可以重载运算符外还可以重载变量类型,但语法比较特殊;

  • 1 不用声明返回值
  • 2 转换的类型就是返回之后,同时也是函数名
#include "stdafx.h"
#include <iostream>
using namespace std;


class MyString {
public:
	MyString(const char * buf) 
	{ m_szBuf = _strdup(buf); }
	~MyString() { free(m_szBuf); }

	//语法比较特殊
	//1. 不用声明返回值
	//2. 转换的类型就是返回之后,同时也是函数名
	operator int() { return strlen(m_szBuf); }
	operator char *() { return m_szBuf; }
	operator char() { return m_szBuf[0]; }
private:
	char * m_szBuf;
};
int main() {
	MyString str("hello world");
	int strLen = (int)str;
	char * pStr = (char *)str;
	char firstChar = (char)str;
	cout << pStr << ":" << strLen << endl;
	return 0;
}

特殊运算符重载(输入输出运算符 )

如果是重载双目操作符(即为类的成员函数),如果运算符重载为成员函数,第一个参数必须是本类的对象。
我们使用>> 或<< 时,而cin与cout是系统默认的关键字而非对象名,而左侧运算量又必须是对象名。所以作为成员函数引用时是以下这个样子的:

Iamclassobj << cout;

可这样的用法并不符合常规的应用习惯。

使用友元函数时可以不强制要求左侧运算量必须是对象本身,为了更好的符合代码书写系统,不改变默认符号的原本意义,所以通常使用友元函数对输入、输出流符号进行运算符重载;

详细代码见以下
【特殊运算符重载(输入输出运算符 )】

#include "stdafx.h"
#include <iostream>
using namespace std;


class MyString {
public:
	MyString(const char * buf) { m_szBuf = _strdup(buf); }
	~MyString() { free(m_szBuf); }

	//输出,返回ostream方便连续输出
	friend ostream& operator<<(ostream & output, MyString &str) {
		return output << str.m_szBuf;
	}
	//输入,返回istream方便连续输出
	friend istream& operator>>(istream & input, MyString & str) {
		return input >> str.m_szBuf; //注意str.m_szBuf的长度
	}
	//成员函数重载
	// ostream & operator >>(ostream & output)
	// {
	// 	output << m_szBuf;
	// 	return output;
	// }

private:
	char * m_szBuf;
};
int main() {
	MyString str1("hello world"), str2("基础最重要");
	cout << str1 << ":" << str2 << endl;
	cin >> str1 >> str2;
	return 0;
}

特殊运算符重载(字符串连接运算符 +)

#include "stdafx.h"
#include <string.h>
#include <malloc.h>

class MyString {
public:
	MyString(const char * buf) { m_szBuf = _strdup(buf); }
	~MyString() { free(m_szBuf); }

	MyString(MyString &obj) { m_szBuf = _strdup(obj.m_szBuf); }
	MyString& operator=(MyString & obj) {
		free(m_szBuf); m_szBuf = _strdup(obj.m_szBuf);
		return *this;
	}
	//成员函数重载
	// MyString operator+(const MyString & str2) {
	// 	int len = strlen(m_szBuf) + strlen(str2.m_szBuf) + 1;
	// 	char * buf = new char[len];
	// 	memset(buf, 0, len);
	// 	strcat_s(buf, len, m_szBuf);
	// 	strcat_s(buf, len, str2.m_szBuf);
	// 
	// 	MyString temp(buf);
	// 	delete[] buf;
	// 	return temp;
	// }
	//友元函数重载
	friend MyString operator+(const MyString & str1,const MyString & str2) {
		int len = strlen(str1.m_szBuf) + strlen(str2.m_szBuf) + 1;
		char * buf = new char[len];
		memset(buf, 0, len);
		strcat_s(buf, len, str1.m_szBuf);
		strcat_s(buf, len, str2.m_szBuf);

		MyString temp(buf);
		delete[] buf;
		return temp;
	}
private:
	char * m_szBuf;
};

int main()
{
	MyString str1("hello"), str2(" world"), str3(" ");
	str3 = str1 + str2;
	str3 = str1 + " world";
	str3 = "hello" + str2;
	return 0;
}

继承与派生

在C++中,如果有一个类B继承了类A,或从类A派生出类B,通常称类A为基类(父类),称类B为派生类(子类),类B不但拥有类A的属性,而且还可以拥有自己新的属性;

  • 单继承:派生类只有一个直接基类的继承方式;
  • 多继承:派生类有多个直接基类的继承方式;
  • 虚函数: 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数;

访问与控制

继承方式:派生类是按指定的继承方式派生的,继承方式有:

  • public : 公有继承

基类的每个成员再派生类种保持同样的访问权限;

  • private:私有继承

基类中的每个成员在派生类中都是private成员,而且它们不能再被派生的子类所访问;

  • protected :保护继承

基类中的public成员和protected成员再派生类中都是protected成员,private成员在派生类中仍为private成员

实例代码

#include "stdafx.h"
#include <iostream>
using namespace std;

class CClassA {
public:
	CClassA(int nNum) { m_nNumA = nNum; }
protected:
	int m_nNumA;
	void fun() {}
private:
	int m_nNumB;
};

class CClassB :private CClassA {
public:
	CClassB(int nNum) :CClassA(nNum) {
		// ......
	}
	void print() {
		fun();
		cout << m_nNumA << m_nNumB << endl;
	}
	CClassA::fun;
	CClassA::m_nNumA;
	//CClassA::m_nNumB;  //父类的私有属性,子类不能访问
};
int _tmain(int argc, _TCHAR* argv[]) {
	CClassB objB(15);
	objB.print();
	objB.fun();
	objB.m_nNumA;
	//objB.m_nNumB;
	return 0;
}

派生类的定义格式

单继承方式

单继承的方式的格式如下:

class <派生类名> :<继承方式> <基类名>
{    
}

实例代码

#include "stdafx.h"

class Base
{
public:
	Base() :m_a(0) , m_b(0)
	{
	}
	void print()
	{
		printf("我是父类");

	}
private:
	int m_a;
	int m_b;
};

class CTest :public Base
{
public:


private:

};


int _tmain(int argc, _TCHAR* argv[])
{
	CTest obj;
	obj.print();
	return 0;
}

多继承方式

多继承派生类有多个基类,基类名之间用逗号分离,每个基类名前都有一个该基类的继承方式说明;
缺省的继承方式为私有继承;

class <派生类名>:<继承方式1> <基类名1>,<继承方式2> <基类名2>
{
}

实例代码

#include "stdafx.h"
class Base1
{
public:
	void FunBase1()
	{
		printf("我是基类1");
	}
};
class Base2
{
public:
	void FunBase2()
	{
		printf("我是基类2");
	}
};
class CTest :public Base1 , public Base2
{
public:
	void FunTest()
	{
		printf("我是子类");
	}
	void fun()
	{
		FunBase1();
	}
};
int _tmain(int argc, _TCHAR* argv[])
{
	CTest obj;
	obj.FunBase1();
	obj.FunBase2();
	obj.FunTest();
	obj.fun();
	return 0;
}

构造析构调用顺序

自己敲了一遍代码,发现派生之后,运行构造函数前是先调用了基类的构造函数,再调用子类的构造函数。而调用析构的时候顺序与调用构造函数的顺序相反,先调用子类的析构函数,再调用基类的构造函数;

#include "stdafx.h"
#include <iostream>
using namespace std;

class CClassA {
public:
	CClassA() { cout << "CClassA()" << endl; }
	CClassA(int nNum) { cout << "CClassA(int)" << endl; }
	~CClassA() {
		cout << "类A析构" << endl;
	};
};
class CClassB :public CClassA {
public:
	CClassB() { cout << "CClassB()" << endl;; }
	//为何这里调用的时候不调用类A的同名函数,而是调用了基类的构造函数??
	CClassB(int nNum)  { cout << "CClassB(int)" << endl; }
	//因为默认情况下首先调用类A的无参默认构造函数,如果想要调用类A有参构造函数需要自定义传参进类A的有参构造函数
	//CClassB(int nNum) :CClassA(nNum){ cout << "CClassB(int)" << endl; } 
	CClassB(int nNumA, int nNumB, int nNumC)
		:CClassA(nNumA), m_nNumB(nNumB), m_nNumC(nNumC) {
		cout << "CClassB(int,int,int)" << endl;
	}
	~CClassB() { cout << "类B析构" << endl; };
private:	int m_nNumB, m_nNumC;
};
int _tmain(int argc, _TCHAR* argv[]) {
	//调用父类对象,并不会调用子类的函数
	CClassA objAA;
	CClassA objAB(16); 
	//调用子类对象,会调用父类的默认构造函数,而传入参数后才可调用父类同名同参函数
	CClassB objA;
	CClassB objB(15);
	CClassB objC(1, 5, 6);

	return 0;
}

派生类的二义性问题(重定义)

当子类与父类中有相同的函数名时,会优先调用子类的成员函数,如果想要调用父类的成员函数,需要指明调用哪个类的成员函数,格式如下:

<对象名>.<父类名>::<成员函数>;
objB.CClassA::printf_A();

例如:


#include "stdafx.h"
#include <iostream>
using namespace std;

class CClassA
{
public:
	void printf_A() { cout << "ClsA printf_A!
" << endl; }
	void printf_B(int nNum) { cout << "ClsA printf_B!
" << endl; }
};

class CClassB :public CClassA {
public:
	void printf_A(int nNum) { cout << "ClsB printf_A!
" << endl; }
	void printf_B() { cout << "ClsB printf_B!
" << endl; }
};

int _tmain(int argc, _TCHAR* argv[])
{
	CClassB objB;
	objB.printf_A(15);
	objB.printf_A();          //子类通过重定义,覆盖掉了父类的方法
	objB.CClassA::printf_A(); //必须通过类名来显式访问被重定义的方法

	objB.printf_B();
	objB.printf_B(15);
	return 0;
}

虚基类

引用虚基类的目的是为了解决二义性问题,使用公共基类在其派生类对象中只产生一个基类子对象,其目的是使公共基类在其派生对象中只产生一个基类子对象;
使用格式如下:

virtual <继承方式> <基类名>
virtual  public  CClassA

实例代码

#include "stdafx.h"
#include <iostream>
using namespace std;

class CClassA {
public:    void fun_a() { cout << "fun_a:CClassA
"; }
};
class CClassB1 : virtual public CClassA {
public:    void fun_b() { cout << "fun_b:CClassB1
"; }
};
class CClassB2 : virtual public CClassA {
public:    void fun_b() { cout << "fun_b:CClassB2
"; }
};
class CClassC :virtual public CClassB1, virtual public CClassB2 {
public:    void fun_c() { cout << "fun_b:CClassC
"; }
};
int _tmain(int argc, _TCHAR* argv[]) {
	CClassC obj;
	obj.fun_a();             //right(虚继承(virtual),解决菱形继承引擎的二义性问题)   
	//obj.fun_b();           //error 虚继承不能解决这个不同作用域内产生函数重载的问题
	obj.CClassB2::fun_b();   //right 所有的二义性问题都能这样解决

	return 0;
}

重定义小结

  • 派生类中构造函数与析构函数的执行顺序相反,先执行基类的构造函数再执行子类的构造函数。析构函数则先执行子类的析构函数,再执行基类的析构函数;
  • 派生类的成员函数优先级别为先执行基类的成员函数,再执行子类的成员函数;
  • 重定义的概念

重定义是子类需要修改或扩展基类的某个成员的功能时需要利用的机制;

重定义分别可以对基类的数据成员的重定义,或对基类成员函数的重定义;

重定义的新成员既可以与基类完全相同,也可以与基类成员函数名相同而参数不同;

  • 1.不管子类重载的成员函数的参与与基类是否完全相同,都会构成重定义;
  • 2.重定义是指在不同的作用域中定义的成员函数名相同,参数不同或相同的情况;
  • 3.重定义的成员会覆盖掉其父类成员;

如果一个派生类有多个直接基类,而这些基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员,C++提供虚基类的方法,使得继承间接共同基类时只保留一份成员。

命名空间

引入了命名空间这个概念,是为了解决命名冲突的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。

命名空间是一个逻辑上的类型组织系统,用它来对程序中的类型进行逻辑上的分组,并使定义在同一个命名空间上的类可利用命名空间直接相互调用;

使用namespace关键字,将库或程序中的C++定义集封装在一个命名空间中,如果其他的定义中有相同的名字,但它们在不同的命名空间,则不会产生命名冲突;

定义命名空间

定义格式:
命名空间的定义使用关键字 namespace,后跟【命名空间的名称】

namespace 命名空间名
{
    //命名空间成员(其他命名空间或类的定义)
}

使用格式:
为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称,如下所示:

#include "stdafx.h"


int main()
{
namespace Outer
{
	int nNumA = 10;
	int fun() {};
	int fun1();

	namespace Inner
	{
		int nNumA = 10;
		int nNumC = 10;
		int fun() {};
		int fun1();
	}
}
int Outer::fun1()
{
}
int Outer::Inner::fun1()
{

}

//1.通过名字单独访问
	Outer::nNumA;

	//2.通过声明特定的名字来访问
	using Outer::fun;
	fun();

	//3.通过声明整个命名空间来访问
	using namespace Outer;
	nNumB;
	fun1();

	//=====内层命名空间,还必须单独在声明==============
	//1.
	Inner::nNumC;
	//2.
	using Inner::nNumC;
	nNumC;
	//3.通过声明整个命名空间来访问
	using namespace Inner;
	Inner::nNumC;
	//==============================================

    return 0;
}

原文地址:https://www.cnblogs.com/17bdw/p/6092297.html