JNI之C++语言二

在上一篇博文JNI之C++语言一中介绍了C++中一些与C有差异或者特有的部分,但是还没有涉及到面向对象的一些特性 。在接下来的两篇博文中主要介绍C++面向对象的一些特性,限于篇幅的关系,本文主要介绍类的定义与访问控制,对象的创建和this指针,构造函数初始化列表,静态成员的初始化,构造函数、析构函数和复制构造函数的定义、作用以及执行时机。有关继承、友元、多态、虚函数等特性下篇博文继续。

类的定义

Java中要求一个文件中最多有一个public类,并且文件名称必须与这个类名相同,如果文件中只包含一个类,则文件名与类名同名。在C++中没有这种要求,其实很多面向对象语言中都没有Java中类名与文件名同名这一说法。

在C++中定义类也是使用class关键字,与Java不同的是,在定义类时访问控制符如private、protected以及public可以抽取出来,将所有相同访问权限的变量、方法定义在一起,而且可以将变量或者方法的定义与实现分离,我们可以在类中定义相关的变量以及方法,然后在类的外部或者在其它文件中去为变量赋值或者实现方法,这时候只需要借助于作用域运算符::,在实现时使用类名::变量或者方法即可。

在C++中有三个访问控制符,分别是private、protected和public,如果什么都不加默认是private。

在C++中也可以定义静态成员,静态成员跟Java类似,并不属于某一个对象,而是对类的所有对象共享的。在调用方式上面也需要注意,静态成员不能使用类似Java中类名.的方式调用,可以通过对象.访问或者使用类名加::访问。

类的静态成员一般在类外面初始化,非静态变量不可以在类外部初始化,非静态函数建议在类外部实现,当定义静态成员时需要使用关键字static,但是在对静态变量赋值或者实现静态函数时不需要使用static关键字。

class Stack{
public:
	Stack(int s);
	int push(int mem);
	int pop(int &num){
		if (memNum == 0){
			return 0;
		}
		num = data[--memNum];
		return 1;
	}
public:
	int *data; //栈数据存储
	int memNum;//栈元素个数
	int size;  //栈大小
	static int objNum;
};

Stack::Stack(int s){
	objNum = 100;
	size = s;
}

int Stack::push(int mem){
	if (memNum == size){
		return 0;
	}
	data[memNum++] = mem;
	return 1;
}
int Stack::objNum = 100;
void main(){
	Stack stack(1);//创建对象

	stack.objNum;
	cout << stack.objNum << " " << Stack::objNum << endl;
}

创建对象

class Circle{
public:
	Circle(){
		x = y = r = 5.0;
	}
	void draw(){
		cout << "draw circle at(" << x << "," << y << "), width radius" << r << endl;
	}
private:
	double x, y;//圆心坐标
	double r;//半径
};

我们知道在Java中使用一般new创建对象,但是在C++中使用new也可以创建对象,不过返回的是一个对象指针,除此之外,还可以使用其它方式创建对象,直接使用声明方式创建对象,而且如果定义类时没有定义构造函数或者只有无参构造函数,声明对象时括号都不需要使用,类似定义基本数据类型变量。如果所定义的类没有默认或者无参构造函数,这时候声明对象时可以使用括号加上参数形式。

如果创建对象时使用=但是没有new的形式,则应该在右侧使用括号。

如果在类中定义了构造函数,则必须要在后续使用之前实现该构造函数,否则会抛出异常,当然了可以不定义,这时候的处理机制类似Java,系统会提供一个默认构造函数。

Circle one;            //声明一个对象one
Circle array[10];      //声明对象数组array
Circle *ptr = &one;    //声明类的一个指针,指向对象one
Circle &obj = one;     //声明类的引用
Circle two = Circle(); //声明一个对象two

Circle *pone, *pten;
pone = new Circle;     //动态创建对象
pten = new Circle[10]; //动态创建对象数组

Circle *pmc = (Circle *)malloc(sizeof(Circle));//动态创建对象

Stack stack(10);            //动态创建对象,向构造函数传参10
Stack *ptr = new Stack(20); 

构造函数

在C++中我们通称为构造函数,在Java中一般称为构造方法,其实一个意思,更多时候是称类中的为构造方法。构造函数可以确保对象在创建后其数据成员可以正确被初始化,如果在定义类时不提供任何构造函数,则会使用默认构造函数。

构造函数也是函数,只是没有返回值而已。在C++中定义函数时可以使用默认值定义函数,所以构造函数也不例外,这样可以有效减少代码重复率,现在Google正则推广的Kotlin语言也可以使用默认值的函数。

一般在创建对象时会调用构造函数,但是有一种情况例外,如果使用malloc()函数创建,这时候不会调用构造函数。

在上述Circle类示例中,如果我们把无参构造函数换成如下构造函数,则在使用时就灵活了许多。

Circle(double a = 5.0, double b = 5.0, double c = 5.0){
	x = a;
	y = b;
	r = c > 0 ? c : 5.0;
}
//创建对象
Circle a;
Circle b(1.0);
Circle c(3.0,4.0);
Circle d(3.0,4.0,5.0);

构造函数初始化列表

在C++中支持构造函数初始化列表,其实这种方式跟构造函数赋值很类似,不同的地方在于,初始化列表是在构造函数后面进行的赋值,定义方式是在构造函数后面使用":"定义初始化列表,不同变量之间使用","分割。

class Student{
public:
	//对变量赋值
	Student(char *name, int age){
		_name = name;
		_age = age;
	}
	//对变量初始化
	Student(char *name, int age):_name("admin01"),_age(24){
		
	}
	void show();
private:
	char *_name;
	int _age;
};
void Student::show(){
	cout << _name << "  " << _age << endl;
}
void main(){

	Student stu("admin",20);//admin01  24
	stu.show();
}

示例代码中通过构造方法初始化列表我们对两个全局变量_name和_age进行了赋值,它们的值分别是"admin01"和24,如果通过该构造方法定义对象,则会将所有的入参在这里都更改为了_name="admin",age=24,所以一般都是如下写法,将构造函数中入参作为变量的初始值。

Student(char *name, int age):_name(name),_age(age){
	//TODO	
}

this指针

在Java中如果想引用一个类的当前对象,我们可以使用this代替当前对象的引用。C++中也有this关键字,在类中表示的是一个this指针,代表了当前对象指向自己的一个指针。this指针是一个const指针,永远指向对象的首地址。

在结构体中,如果是结构体指针变量访问结构体中成员我们使用的是->,在类中,如果是类指针,在访问成员变量时也是使用->。

在上述Student示例中,如果在构造函数以及show()方法中使用this操作变量,这样构造函数的入参就可以和全局变量重名使用了。

Student(char *name, int age){
	this->name = name;
	this->age = age;
}
void Student::show(){
	cout << this->name << "  " << this->age << endl;
}

this指针有一个重要作用,就是可以返回对象自身,从而实现成员函数的链式调用。我们知道this是指向对象的首地址,那么*this就可以获取对象自身了。如果感觉这里有些绕,其实这里可以借助于基本数据类型指针变量理解一下。

int i = 10;
int *p = &i;
cout << *p << "  " << p << endl;//10  00F5FE94

this就相当于上面示例中的p指针,如果直接输出p的话,其实是一个地址,*p则代表了p所指向的变量的值,所以*this则代表了对象自身。

Student & showName();
void showAge();
//...
Student & Student::showName(){
	cout << this->name << endl;
	return *this;
}

void Student::showAge(){
	cout << this->age << endl;
}

void main(){

	Student stu("admin",20);
	stu.showName().showAge();

}

上述示例中有一点需要注意,showName()函数返回的是Student对象的引用,一定要返回引用类型,如果不是引用类型,虽然在方法调用时也可以正常输出,可是这时候的对象已经不是stu了,其实是stu对象的一个copy,这一部分在下文介绍复制构造函数时介绍。

析构函数

析构函数跟构造函数类似,前面也没有返回值,构造函数在创建对象时调用,析构函数在对象销毁时调用,析构函数类似构造函数取“反”,所以在析构函数前面有一个“~”操作符。

析构函数可以在对象销毁时做一些清理工作,如释放对象在生存周期内申请的动态内存。

由于对象可以分为全局对象、局部动态对象以及静态对象,所以对象销毁的时机也不相同,局部对象一般在退出作用域时被执行,全局对象和静态对象都是跟程序的生成周期相同,只有调用exit或者程序退出时才会执行。

Student::~Student(){
	cout << "end:" << this->name << endl;
}
void func(){
	Student stu("admin", 20);
	stu.showName().showAge();
}

void main(){

	func();
	Student *pstu = new Student("Lily", 21);
	pstu->showName().showAge();
	delete pstu;
	
}

上述示例,当func()函数执行完成后就会输出end:admin,当main()函数中代码执行delete之后,这时候才会输出end:Lily。

复制构造函数

复制构造函数有时也称为拷贝构造函数,其实就是可以根据某个类的对象复制出一个完全相同的对象的构造函数,是的,也是一个构造函数,跟Java中clone相似。复制构造函数其实就是入参为该类对象的引用,一般情况下都是传引用的方式,而且入参使用const修饰。

如果类定义时没有定义复制构造函数,编译时会系统会自动生成一个默认的复制构造函数。

Student::Student(const Student &stu){
	this->name = stu.name;
	this->age = stu.age;
	cout << "copy" << endl;
}
void func(){
	Student stu("admin", 20);
	Student st = stu;
}

运行上述代码,这时候就会执行复制构造函数。复制构造函数一般在如下三种情况下都会被调用。

直接将一个对象赋值给另外一个对象,如上述示例。 执行函数返回值是一个对象,上文示例中如果showName函数返回的是非引用类型的对象。 对象作为函数的入参传入,这时候也会调用复制构造函数。

复制构造函数容易引发深拷贝和浅拷贝问题,默认复制构造函数时浅拷贝,类似Java中深克隆和浅克隆,浅拷贝时,如果两个对象其中某些成员共用同一个内存空间,这时候如果对一个对象某些成员的释放操作,可能引发另外一个对象访问问题。

在上面的Student类的示例中,我们将复制构造函数中name使用malloc()进行动态分配内存空间,然后在析构函数中进行free()操作,这样如果通过复制构造函数生成的对象再次调用name值时就会引发异常了,因为name所占的空间已经被释放了。

为了解决浅拷贝这个问题,在复制构造函数中需要将某些动态分配内存的数据来重新分配新空间,这样就可以保持二者数据的独立性。

Student(char *name, int age){
	int len = strlen(name) + 1;//+1是为字符串结束符\0分配的空间
	this->name = (char *)malloc(len);
	strcpy(this->name,name);
	this->age = age;
}
Student::Student(const Student &stu){
	int len = strlen(stu.name)+1;
	this->name = (char *)malloc(len);
	strcpy(this->name,stu.name);
	this->age = stu.age;
	cout << "copy" << endl;
}

小结

通过本文,首先需要掌握如何定义类和创建对象,在定义类时需要区分静态成员和非静态成员的差异。其次要知道每一个对象都维护着一个this指针,该指针指向的是对象的首地址。最后就是需要理解三种函数的使用方式和调用时机,这三种函数分别是构造函数、析构函数和复制构造函数,并理解构造函数初始化列表和赋值的差别,在使用复制构造函数时一定要注意浅拷贝陷阱。

有关继承、友元、多态、虚函数下文继续介绍。

评论

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