C++在C的基础上引入了继承、多态和重载的特性,它们很大程度上建立在类的基础上。类的结构:成员变量和成员函数,每本C++的教材上都会写,每个初学C++的人也一定能掌握。但是只了解这些,学习到的不能称为C++,应该叫做C with class:初学者只把class当作是一种复杂的、能绑定函数的数据结构在使用。所以今天先从类的起点和终点——建立和销毁说起。
C++中有一类特殊的成员函数,名字和类名相同(析构函数前面加上~),无返回值,也无需用户显式调用,创建和销毁对象时自动执行,这种特殊的成员函数就是构造函数(Constructor)和析构函数( Destructor )。
1.从最简单的构造函数开始
#include
此class仅供参考,使用了两个对象,其中sam在栈上,jack在堆上。为了安全使用字符指针,在析构函数中使用new开辟空间,然而这是分配在堆上的空间,需要使用完之后在其他地方安全释放,故在析构函数里面使用这个操作,当然new和delete相关会在后续的内存分配文章中介绍。如果把类中构造函数和析构函数的声明、类外构造函数和析构函数的定义注释掉,编译器报告下列错误:
原因是因为笔者传入的是字符串常量,没有析构函数,编译器无法自动完成类型转换,存入数据,更无从谈起下一步的释放空间了。
通常情况下,不涉及类型转换、空间分配和释放、指针操作等复杂功能的构造函数和析构函数,是可以不写出,由编译器直接提供默认的People(){},没有参数也不执行任何动作;它们必须是public属性,否则在外部无法调用;没有返回值——它隐藏的返回值是this指针。
这里补充一下为什么不可以有具体的返回值:
c++构造函数有返回值吗?www.zhihu.com从实际上说,此处的ECX就是指针this,实际上对象在调用构造函数之前就存在。从语义上说,举一个建立在这个错误上的例子:
class A{
public:
int A():x(0){return 1;}
A(int i):A(i){}
private:
int x;
}
假设我们这样使用第一个构造函数,那么它返回1给临时对象,对象接收到返回值,认为是一个参数,使用第二个构造函数,这样以来就不知道创建的对象究竟是A(0)还是A(1)了,产生了二义性。
构造函数的调用是强制性的,只要在类中定义了构造函数,创建对象时一定调用。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中一个构造函数来匹配,也即创建对象是只有一个构造函数会被调用。对于文章最前面提出的例子,如果使用People sam()或者new sam()(无参数不加括号也可以)就是错误的,没有无参数的构造函数。用户自定义构造函数之后,编译器都不再自动生成。
如果构造函数的内容比较简单,可以采用初始化列表的办法:
#include<iostream>
#include<cstring>
class People{
private:
const char* name;
int age;
public:
People(const char* name, int age);
~People();
void SetAge(int age);
int GetAge();
};
People::People(const char *name, int age):name(name), age(age){};
People::~People(){
this->name = nullptr;
}
void People::SetAge(int age){
this->age = age;
}
int People::GetAge(){
return age;
}
int main(){
const char *p = "sam";
People sam(p, 20);
sam.SetAge(15);
std::cout<<sam.GetAge()<<std::endl;
const char *q = "jack";
People *jack = new People(q, 16);
jack->SetAge(16);
std::cout<<jack->GetAge()<<std::endl;
return 0;
}
在这个例子中,初始化了const成员变量。为什么const成员变量只能在初始化列表中使用呢?一、常量只能初始化不能赋值,函数体内是赋值,若我们将const和引用类型的成员变量放在构造函数的函数体内的话。那么const和引用类型的变量将会没有初值,即没被初始化;二、构造函数不能被声明为const函数,因此当我们创建一个类的const对象时,直到构造函数完成初始化的过程,对象才真正取得其“常量”的属性,因此,构造函数在const对象的构造过程中可以向其写值;三、主要是性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。
#include<iostream>
#include<cstring>
class People{
private:
const char* name;
int p_age;
int p_id;
public:
People(const char* name, int age, int id);
~People();
void SetAge(int age);
int GetAge();
};
People::People(const char *name, int age, int id):name(name), p_id(id), p_age(p_id){}
People::~People(){
this->name = nullptr;
}
void People::SetAge(int age){
this->p_age = age;
}
int People::GetAge(){
return p_age;
}
int main(){
const char *p = "sam";
People sam(p, 20, 14);
std::cout<<sam.GetAge()<<std::endl;
const char *q = "jack";
People *jack = new People(q, 16, 13);
std::cout<<jack->GetAge()<<std::endl;
return 0;
}
需要特别注意的是,成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关,把上面的例子修改成这样:
People::People(const char *name, int age, int id):name(name), p_id(id), p_age(p_id){}
输出的结果是两个0,显然是错误的。因为实际上,它等效于这样。在还变量还未初始化的时候就使用。
People::People(const char *name, int age, int id){
this->name = name;
this->p_age = this->p_id;
this->p_id = id;
}
到此为止介绍了简单的构造函数特性,这个部分很长,估计会花好几篇写完。
(如有转载请注明作者与出处,欢迎建议和讨论,thanks)