众所周知,面向对象编程有三大特性:封装、继承以及多态性。而在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();
};
一些特殊却非常重要的函数(构造函数、析构函数、拷贝构造函数、友元函数)。
其中,构造函数、析构函数以及拷贝构造函数,如果自己没有定义,那么编译器会自动生成一个缺省函数。
- 构造函数
构造函数与类同名,并且不返回任何值;
构造函数总是在创建对象时被调用,通常可以被用作初始化类的成员变量;
构造函数有且只有一个。
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){}; //包含初始化列表,声明需要加上{}
};
- 析构函数
析构函数与类同名,前面加上~ ,并且没有参数传入,没有返回值。
析构函数在对象被销毁时自动调用;
析构函数有且只有一个。
class A
{
public:
~A();
};
- 拷贝构造函数
拷贝构造函数是深拷贝(不仅拷贝指针的值,还会分配新的内存),我们通常会选择以引用的方式传入当前类的对象。
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;
}
- 友元函数
若想从外部访问私有数据成员和方法,那么需要声明友元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{};
处理继承时的几点小问题
-
构造顺序 & 析构顺序
创建子类对象的时候,先实例化父类对象,再实例化子类对象;在实例化的时候,先实例化成员属性,再调用构造函数。
class A
{
int a; // 1
public:
A(){}; // 2
};
class B:public A
{
int b; // 3
public:
B(){}; // 4
};
析构顺序与构造顺序完全相反。
- 子类覆盖问题
- 如果子类实现了从基类继承的函数,返回值也相同,那么就相当于覆盖了父类的该函数。这也是多态性存在的必要性之一。
- 如果要调用被覆盖的父类的函数,那么在调用时+父类名+作用域解析运算符
- <父类名>::<函数名>(参数表);
多态性(重载 & 虚函数)
多态性可以分为编译时多态性,以及运行时多态性。分别对应静态链编和动态链编。相对应的知识点是重载和虚函数。
函数重载 & 运算符重载
- 函数重载
函数重载在举构造函数例子的时候已经用到了,粗浅的理解就是,同一个函数名,但是参数表不一样,可以是参数的类型,个数,甚至是顺序。
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;
};//声明纯虚函数的一般形式