JNI之C++语言三

在上文介绍了面向对象的部分特性,如类的定义和对象的创建,包括常用的构造函数、析构函数以及复制构造函数的使用。本文继续介绍面向对象的其它特性,包括单继承多继承以及如何解决多继承中的二义性,虚函数的使用和多态的实现,友元函数和友元类的使用。

上述特性涉及到friend和virtual两个关键字的使用,friend是在使用友元时使用,而virtual关键字在定义抽象类和实现时多态时使用。

友元函数和友元类

如果想要访问一个类的私有属性,我们可以暴露一个公有方法,在该公有方法中对私有属性进行访问处理。在C++中虽然可以面向对象开发程序,但是某些情况下,还是会有一些面向过程的处理逻辑,这时候位于类之外的函数想要访问类中的私有数据,那应该如何处理呢?C++为我们提供了友元机制,可以借助于友元函数或者友元类进行访问控制。

虽然友元机制可以给程序带来极大的便利,但是一般不建议使用友元。

class A{
	friend void changeValue(A &a,int i);
public:
	A(int x){
		this->x = x;
	}
	void print();
private:
	int x;
};
void A::print(){
	cout << this->x << endl;
}
void changeValue(A &a,int i){
	a.x = i;
}
void main(){
	A a(12);
	a.print();  //12
	changeValue(a,100);
	a.print();  //100
}

友元函数一般会把对象的引用或者指向对象的指针作为入参,否则的话直接把对象作为入参会在函数体内生成一个拷贝对象,就不是原对象本身了。

如果一个类A想要调用另外一个类B的方法或者变量,可以将另外一个类B的对象作为类A的一个成员,或者使用继承,让A类继承B类,这样也可以访问部分非公有属性。但是如果两个类A和B没有上述的依赖关系,这时候,而且A类在某些业务场景下需要对B类的私有属性进行更改,这时候我们就可以借助友元类实现。

友元类的声明并不具有传递性,类A声明类B是友元类,而类B声明类C是友元类,而类C不意味着就是类A的友元类。

class B;// 声明类B
class A{
public:
	void setB(B &,int);
	void printB(B &);
};

class B{
	friend class A;
private:
	int data;
};
// 实现必须位于类B定义的后面
void A::setB(B &b, int i){
	b.data = i;
}
void A::printB(B &b){
	cout << b.data << endl;
}

void main(){
	A a;
	B b;
	a.setB(b,120);
	a.printB(b); //120
}

继承

在C++中是支持类的多继承的,我们知道在Java中类只有单继承,当然了单继承有单继承的好处,因为多继承容易造成二义性,比如,一个子类Child继承了两个父类A类和B类,但是A类中有方法actionMethod,B类中也有actionMethod,如果子类Child调用actionMethod方法时究竟使用了哪个类的方法?这个问题在下文介绍继承时会有详细的讲解。

在C++中继承的实现方式与Java不同,Java中使用关键字extends实现继承,而且只支持单继承的方式,但是C++使用的是运算符":"实现继承,并且支持多继承。

Java中继承某个类,子类的最前面加上访问控制符,而C++中是在所继承的父类前面使用的访问控制符。

using namespace std;
//People
class People{
public:
	People(char *name, int id);
	~People();
	void show();
private:
	char *name;
	int id;
};
People::People(char *name, int id){
	this->name = new char[strlen(name) + 1];
	strcpy(this->name, name);
	this->id = id;
	cout << "people constructor" << endl;
}
People::~People(){
	delete[]name;
	cout << "people delete" << endl;
}
void People::show(){
	cout << this->name << "  " << this->id << endl;
}

//Student
class Student :public People{
public:
	Student(char *name, char *school, int id);
	~Student();
	void show();
private:
	char *school;
};

Student::Student(char *name, char *school, int id) :People(name, id){
	this->school = new char[strlen(school) + 1];
	strcpy(this->school, school);
	cout << "student constructor" << endl;
}
Student::~Student(){
	delete[] school;
	cout << "student delete" << endl;
}
void Student::show(){
	People::show();
	cout <<"school:"<<this->school<< endl;
}

void func(){
	People people("people",1001);
	Student stu("student","school",90001);
	people.show();
	stu.show();
}
//people constructor
//people constructor
//student constructor
//people  1001
//student  90001
//school:school
//student delete
//people delete
//people delete
void main(){
	
	func();

}

子类对象在创建时会首先调用父类的构造函数,如果没有定义构造函数,系统会使用默认构造函数,这一点跟Java很相似。在对象销毁时,子类和父类之间析构函数执行顺序跟创建时相反,会先执行子类的析构函数,然后才是父类的析构函数。

在Java中,如果子类重写了父类的方法,在外部是不能使用子类对象调用父类已经被重写的方法,但是在C++中,是可以的,调用方式如下:

stu.People::show();// student  90001

在Java中子类对象和父类对象是可以相互转换的,子类对象转换为父类对象而不需要强制转换,但是父类对象转换为子类对象需要强制转换。但是在C++中有所不同,子类对象可以转换为父类对象,但是父类对象无法转换为子类对象,即使强制转换也不可以。

People p = stu;
Student s = (Student)people; //转换出错

多态与虚函数

面向对象的三大特性:封装、继承和多态,多态分为方法重载与重写,重载是在同一个类中同名方法的不同实现,重写是在子类与父类之间,子类重写父类的方法。在上面介绍子类对象和父类对象相互转换时说了,父类对象不能转换为子类对象,否则一定会报错,编译就不能通过。

有一种方式可以让父类类型转换为子类类型,那就是使用类指针,指针是地址,所以一个对象的如果是指针类型变量,那么该指针指向的是对象的首地址,两个同为地址类型的变量当然可以相互转换了。

我们知道函数的调用在编译期间就可以确定,即一个对象只可以调用该对象所在类的函数,这种方式被称为静态绑定。在Java中,如果一个父类型对象被某个子类对象赋值后,一旦子类重写了该父类的方法,这时候再次用父类类型对象调用被已经被重写过的方法,实际上调用的是子类重写的方法,也就是说对同一个函数或者方法的调用,不同的对象会执行不同的方法,这种方式就是动态绑定

动态绑定需要使用类指针或者引用调用虚函数才可以实现。

这里我们仍然使用上述继承中的示例。

People *p = &stu;
p->show();//student  90001

Student *s =(Student*)&people;
s->show();//student  90001

上述这种操作就和Java很类似了,父类类型指针和子类类型指针可以相互转换,子类指针类型转父类型无需强制转换,否则需要强制转换。这里可以总结一下了,在C++面向对象的特性中,有关类指针或者引用的操作才跟Java中对象类型的操作类似,单纯讨论C++中对象类型跟Java对象类型是还是由很大的差异性的,这样理解,Java中对象类型其实就是一种引用类型,引用类型和引用类型操作才有相似之处。

在上述示例中运行p->show()结果没有输出Student的school信息,说明这时候调用的还是父类People中的show()函数,并没有达到类似Java中多态的实现,其实这时候我们只需要在父类show()函数声明时,在函数前面添加一个virtual关键字即可实现多态。

class People{
public:
	People(char *name, int id);
	~People();
	virtual void show();  //函数前面添加virtual关键字
private:
	char *name;
	int id;
};

如此再执行上面的方法调用,这时候就会输出school:school信息了,上述就是使用C++的方式实现的多态示例。

抽象类

使用virtual修饰的函数为虚函数,如果这个函数没有函数体,则称为纯虚函数,纯虚函数在声明时要初始化为0,包含纯虚函数的类叫抽象类。

#define PI 3.14
using namespace std;
//抽象类
class Shape{
public:
	virtual double area() = 0;
	virtual void show() = 0;
};
class Circle :public Shape{
public:
	Circle(double a = 0.0, double b = 0.0, double c = 1.0){
		x = a;
		y = b;
		r = c;
	}
	double area();
	void show();
private:
	double x, y;//圆心坐标
	double r;//半径
};
double Circle::area(){
	return PI*r*r;
}
void Circle::show(){
	cout << "show circle" << endl;
}

void callArea(Shape &shape){
	shape.show();
	cout << shape.area() << endl;
}

void main(){
	Circle circle(0.0,0.0,2.5);
	callArea(circle);
}

多重继承

class A{
public:
	char *name;
};
class A01 :public A{

};
class A02 :public A{

};
class A001 :public A01, public A02{

};

在上述示例中,如果我们新创建一个A001的对象,然后直接使用A001对象调用name成员,系统会提示“name指示不明确”错误的,因为不知道name成员是继承自类A01还是继承自类A02,这就是多重继承中引发的二义性。

如何解决二义性呢?其实上面继承时介绍了一种方式,就是子类对象可以加上类名以及二目作用域运算符,格式为a001.A01::name。

A001 a001;

a001.A01::name = "from A01";
a001.A02::name = "from A02";
cout << a001.A01::name << endl; //from A01
cout << a001.A02::name << endl; //from A02

为了解决二义性问题,还可以使用虚继承,上述示例中就是一个典型的菱形继承方式,A01与A02继承自A,然后A001继承自A01和A02。如果直接使用继承原先是直接将基类的内存空间拷贝一份来实现的,而虚继承则用一个虚基类指针来指向虚基类,避免基类的重复,这样可以确保同名的成员变量只保留一份拷贝

a001.A01::name = "from A01";
a001.A02::name = "from A02";
a001.name = "from A";
cout << a001.A01::name << endl; //from A
cout << a001.A02::name << endl; //from A
cout << a001.name << endl;      //from A


a001.name = "from A";
a001.A01::name = "from A01";
a001.A02::name = "from A02";
cout << a001.A01::name << endl; //from A02
cout << a001.A02::name << endl; //from A02
cout << a001.name << endl;

小结

C++面向对象的特性基本上介绍完了,相信你对面向对象语言应该有了一个更为清晰深刻的认识。封装其实就是对类的封装,继承其实也好理解,不过要注意处理好多继承中的二义性,多态,注意子类和父类之间需要借助于virtual关键字实现多态。对构造函数不用多说了,但是C++还有另外两种类构造函数的函数,它们分别是析构函数和复制构造函数,一定要知道这两种函数的使用方式和调用时机。

C++还有些知识点需要整理一下,运算符重载、函数模板以及常见的类型转换方式,下篇再做介绍。

评论

您确定要删除吗?删除之后不可恢复