c++学习之路之封装性、继承、多态性(包括几乎所有常见语法)

众所周知,面向对象编程有三大特性:封装、继承以及多态性。而在c++基础语法学习中,三者分别对应类与对象、继承问题以及重载及虚函数问题。本文将集中梳理一下有关这三个特性的相关语法。

封装性(类与对象)

通俗来讲,如果类是模板,那么对象就是实例。一个类中可以包含诸多属性和方法,但这仅仅是蓝图。如果需要实现,那么需要根据类实例化一个对象,然后就可以通过对象,对类的成员方法和属性进行访问。

类的声明及函数的实现

在一个类里面,可以包含诸多的数据成员(属性、状态)以及成员函数(方法、行为)
1. 类的声明

class Base
{
	int x;
	void print();
	...
};

2. 函数的实现

函数实现可分为类内实现类外实现

class Base
{
	void print_1(){cout << "hello world!" << endl;} // 类内实现
	void print_2(int a);
};
void Base::print_2(int a)
{
	cout << a << endl; // 类外实现
}

3. 实例化对象并访问成员

Base b; // 栈
b.print_1(); // 无参数传入
b.print_2(11); // 有参数传入

Base *p = new Base(); // 堆,动态内存
(*p).print_1(); // 句点运算符
p->print_1(); // 指针运算符
delete p; // 主动回收

Base bb;
Base* pp = &bb; //指向对象的指针
pp->print_1();

4. 类的关键字

我们通常将类内的属性和方法分成三类:public(公有)、private(私有)和protected(保护)
public:自己、派生类、外部均可以访问;
private:仅自己可以访问;
protected:仅自己和派生类可以访问。
注:如果某个属性或者方法没有注明关键字,那么当做是private看待。

class Base
{
	int x;
protected:
	string Name;
public:
	Base();
};

一些特殊却非常重要的函数(构造函数、析构函数、拷贝构造函数、友元函数)。

其中,构造函数、析构函数以及拷贝构造函数,如果自己没有定义,那么编译器会自动生成一个缺省函数。

  1. 构造函数

构造函数与类同名,并且不返回任何值
构造函数总是在创建对象时被调用,通常可以被用作初始化类的成员变量;
构造函数有且只有一个

class A
{
	string N;
	int a;
	
	public:
		A(); //构造函数
		A(string Name, int age); 
		A(string Name, int age=11); //带有默认值
		A(string Name, int age):N(Name),a(age){}; //包含初始化列表,声明需要加上{}
};
  1. 析构函数

析构函数与类同名,前面加上~ ,并且没有参数传入没有返回值
析构函数在对象被销毁时自动调用
析构函数有且只有一个

class A
{
public:
	~A();
};
  1. 拷贝构造函数

拷贝构造函数是深拷贝(不仅拷贝指针的值,还会分配新的内存),我们通常会选择以引用的方式传入当前类的对象。

class A
{
	char* Buffer;
public:
	A(const A& Input);
};
A::A(const A& Input)
{
------------通常在拷贝构造函数中会使用以下的方法-------------
	if(Input.Buffer!=NULL) //Buffer为指向对象的指针
		{
			Buffer = new char[strlen(Input.Buffer)+1]; 
			strcpy(Buffer,Input.Buffer);
		}
	else
		Buffer = NULL;
}
  1. 友元函数

若想从外部访问私有数据成员和方法,那么需要声明友元firend(友元函数非必须)。
注:在当前类,声明允许访问的外部函数或者类。

class A
{
	private:
		int age;
		friend void C(const A& a); //将外部函数C声明为友元
		friend class D; //将类D声明为友元
};
void C(const A& a)
{
	cout << age << endl; //允许访问A中private的age
}
class D
{
	...
};

保留关键字this指针

this指针包含的是当前对象的地址,事实上在类成员方法调用其他成员方法的时候,都会隐式传递this指针。虽然在实际编程中,this大多数情况下是可选的,但还是非常重要的一个概念。


继承性

如果类之间有相似的属性,那么可以选择使用继承,提高代码的效率。由此,可以引出父类(基类)和子类(继承类)的概念,两者是继承与派生的关系。通俗地说,父类可以看成是子类的子集。

派生方式(公有、私有、保护)

公有继承 public最常用,基类的公有成员和保护成员都相当于派生类的公有成员,派生类可以通过自身的成员函数访问它们,也可以在派生类外通过派生类对象访问。
私有继承 private:基类的所有成员和方法都是私有的,只能在继承类中使用,不能通过继承类的实例来访问
保护继承 protected基本同private,区别在于,保护继承可以在子类的子类中访问父类,但是私有继承不行。

class Base
{
...
};
class Deriverd:<派生方式> Base
{
...
};

多继承

class A{};
class B{};
class C: public A, private B //同时继承了A,B
{};

虚继承

虚继承用到了多态性的知识,语法上比较简单,所以还是放在这里一并解释。虚继承可以解决常见的菱形问题
比方说,B,C同时继承自父类A,这时,D又需要多继承B,C。这时如果不使用虚继承,那么创建D实例的时候,会自动创建两个A实例,这时不仅占用更多内存,还会在用D实例访问A成员时可能会出错。

class A{};
class B:public virtual A{}; //使用关键字virtual
class C:public virtual A{};
class D:public B, public C{};

处理继承时的几点小问题

  1. 构造顺序 & 析构顺序

    创建子类对象的时候,先实例化父类对象,再实例化子类对象;在实例化的时候,先实例化成员属性,再调用构造函数。

class A
{
	int a; // 1
public:
	A(){}; // 2
};
class B:public A
{
	int b; // 3
public:
    B(){}; // 4
};

析构顺序与构造顺序完全相反。

  1. 子类覆盖问题
  • 如果子类实现了从基类继承的函数,返回值也相同,那么就相当于覆盖了父类的该函数。这也是多态性存在的必要性之一。
  • 如果要调用被覆盖的父类的函数,那么在调用时+父类名+作用域解析运算符
  • <父类名>::<函数名>(参数表);

多态性(重载 & 虚函数)

多态性可以分为编译时多态性,以及运行时多态性。分别对应静态链编和动态链编。相对应的知识点是重载和虚函数。

函数重载 & 运算符重载

  • 函数重载

函数重载在举构造函数例子的时候已经用到了,粗浅的理解就是,同一个函数名,但是参数表不一样,可以是参数的类型,个数,甚至是顺序。

void A();
void A(int a);
void A(int a, double b);
void A(double b, int a);
  • 运算符重载

通过重新定义普通运算符,来实现用户定义类型的计算,通常是对函数进行运算操作。要注意的是,重载的运算符含义应与原含义相近;重载之后的运算符原来的功能依旧存在

<返回类型> operator <运算符>(参数表);//一般形式

运算符重载可以分为两种,一种重载为类的成员函数,一种重载为类的友元函数。两者的区别在于,如果重载为类的成员函数,那么会自动存在一个this指针,所以参数可以比重载为类的友元函数的情况少一个
常见运算符又主要可分为单目运算符和双目运算符。
注::: ?: . * sizeof 不能被重载;= [] () -> new delete 只能被重载为成员函数

------------------------单目运算符---------------------------------
class A
{
	int x,y;
public:
	A(int a=0,b=0):x(a),y(b){}
	A operator  ()
	{
		x++;y++;
		return *this;
	} //单目运算符重载为成员函数
	friend A operator --(A obj); //单目运算符冲仔为友元函数 
};
A operator --(A obj)
{
	obj.x--;
	obj.y--;
	return obj;
}

------------------------双目运算符---------------------------------
class B
{
	double x,y;
public:
	B (double xx, double yy):x(xx),y(yy){}; //构造函数
	B operator +(B b) 
	{
		B temp;
		temp.x = x+b.x;
		temp.y = y+b.y;
		return temp;
	}	// 双目运算符+重载为成员函数
	friend bool operator ==(const B&, const B&); //双目运算符==重载为友元函数
};
bool operator ==(const B& b1,const B& b2)
{
	return ((b1.x == b2.x) && (b1.y == b2.y));
}
int main()
{
	B b1(1,2),b2(3,4);
	B b3 = b1+b2; //调用重载的+
	if(b1==b2) //调用重载的==
		cout << "same";
	else
		cout << "different";
	return 0;
}

类型转化函数

<类名>::operator <类型名a>() { return a类型的实例;}
将类实例的对象转换为其他类型

class A
{
 	int x;
 public:
 	A(int t=0){x=t;}
 	operator int()
 	{
 		return x;//转化为int 类型
 	}	
};
int main()
{
	A obj();
	int a = obj;
}

重载赋值运算符 & 拷贝构造函数

调用重载赋值运算符和拷贝构造函数都可以避免“浅复制”,区别在于,重载赋值运算符是改变现有的值,而调用拷贝构造函数是创建一个新对象。弄清楚下面一段程序,对拷贝构造函数、运算符重载、构造函数、析构函数、类型转换函数以及动态内存都会有进一步理解。

#include <iostream>
#include <cstring>

using namespace std;

class myString
{
    char *Buffer;
public:
    myString(const char *Input);//构造函数
    myString(const myString &);//拷贝构造函数
    myString &operator=(const myString &);//赋值运算符重载
    ~myString();//析构函数
    operator const char *()//类型转换函数,在输出中用到
    {
        return Buffer;
    };
};

myString::myString(const char *Input)
{
    if(Input!=NULL)
    {
        Buffer = new char[strlen(Input) + 1];
        strcpy(Buffer, Input);
    }
    else
    {
        Buffer = NULL;
    }
}

myString::myString(const myString& Copy)
{
    if (Copy.Buffer != NULL)
    {
        Buffer = new char[strlen(Copy.Buffer) + 1];
        strcpy(Buffer, Copy.Buffer);
    }
    else
    {
        Buffer = NULL;
    }
}

myString& myString::operator=(const myString& Copy)
{
    if((this!=&Copy)&&(Copy.Buffer!=NULL))
    {
        if(Buffer!=NULL)
            delete[] Buffer;
        Buffer = new char[strlen(Copy.Buffer) + 1];
        strcpy(Buffer, Copy.Buffer);
    }
    return *this;
}

myString::~myString()
{
    if(Buffer!=NULL)
        delete[] Buffer;
}

int main()
{
    myString string_1("hello");
    myString string_2("world");

    cout << "原始:" << string_1 << " "<< string_2 << endl;

    myString string_3(string_1);
    cout << "拷贝构造:" << string_3;

    string_2 = string_1;
    cout << "赋值运算符重载:" << string_2;

    return 0;
}

占位符
前缀 ++obj
<返回值类型> <类名>::operator ++ ();
后缀 obj++
<返回值类型> <类名>::operator ++(int); //其中int为占位符

虚函数

虚函数的存在,使我们可以通过基类的引用或指针来访问派生类的成员!

virtual <返回值类型> <成员函数名>(虚函数表);//虚函数声明的一般形式

注:派生类中的virtual关键字可以省略;普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数没有虚函数!

class Base
{
public:
	virtual void print()
	{cout << "hello\n";)
};
class Derived:public Base
{
public:
	void print()
	{cout << "world";)
};
void temp(Base &b)
{
	b.print();
}
int main()
{
--------------基类的引用------------------
	Base b1;
	Derived d1;
	temp(b1);
	temp(d1);
--------------基类的指针------------------
	Base *p;
	Derive d2;
	p = &d2;
	p->print(); 
}

虚析构函数

virtual ~Base(){}

在需要多态性的场合,我们通常习惯于把析构函数定义成虚函数。

纯虚函数 & 抽象类 &接口类

在基类中不给出实现的成员函数,称作纯虚函数
而包含纯虚函数的类成为抽象类
当抽象类中所有成员函数都是纯虚函数是,那么这个类被成为接口类
注:抽象类不能实例化。

class <类名>
{
virtual <返回类型> <成员函数名> (参数表)=0;
};//声明纯虚函数的一般形式

发布了2 篇原创文章 · 获赞 0 · 访问量 74

猜你喜欢

转载自blog.csdn.net/dawnwang2000/article/details/105092876