构造函数(Constructor)
- 构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
- 特点
- 函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数
- 一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象
- 注意
- 通过malloc分配的对象不会调用构造函数
- 因为malloc是C的函数,不会调用C++的东西
- 一个广为流传的、很多教程\书籍都推崇的错误结论:
- 默认情况下,编译器会为每一个类生成空的无参的构造函数- 正确理解:在某些特定的情况下,编译器才会为类生成空的无参的构造函数
- (哪些特定的情况?以后再提)
构造函数的调用
struct Person{
int m_age;
Person(){
cout << "Person()" << endl;
}
Person(int age){
cout << "Person(int age)" << endl;
}
};
// 全局区
Person g_p1; // 调用Person()
Person g_p2(); // 函数声明,函数名字是g_p2
Person g_p3(20);// 调用Person(int)
int main() {
// 栈空间
Person p1; // 调用Person()
Person p2(); // 函数声明,函数名字是p2
Person p3(20); // 调用Person(int)
// 堆空间
Person *p4 = new Person; // 调用Person()
Person *p5 = new Person(); // 调用Person()
Person *p6 = new Person(20);// 调用Person(int)
return 0;
}
默认情况下,成员变量的初始化
如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化
struct Person{
int m_age;
};
// 全局区
Person g_p1; // 成员标变量初始化为0
int main() {
// 栈空间
Person p1; // 成员标变量不初始化
// 堆空间
Person *p2 = new Person; // 成员标变量不初始化
Person *p3 = new Person(); // 成员标变量初始化为0
Person *p4 = new Person[3]; // 成员标变量不初始化
Person *p5 = new Person[3](); // 3个对象的成员标变量初始化为0
Person *p6 = new Person[3]{}; // 3个对象的成员标变量初始化为0
return 0;
}
成员变量的初始化
对象初始化
Person(){
memset(this, 0, sizeof(Person));
}
析构函数(Destructor)
- 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作
- 特点:
- 函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数
- 注意:
- 通过malloc分配的对象free的时候不会调用构造函数
- 构造函数、析构函数要声明为public,才能被外界正常使用
对象的内存管理
- 对象内部申请的堆空间,由对象内部回收
- 多注意setter和析构的内存管理
声明和实现分离
就是类的域不同::
略
命名空间
- 命名空间可以用来避免命名冲突
- 命名空间不影响内存布局
namespace YY {
int g_age;
class Person{
public:
Person(){
cout << "Person()" << endl;
}
~Person(){
cout << "~Person()" << endl;
}
void run(){
cout << "run()" << endl;
}
};
};
int main() {
YY::g_age = 20;
cout << "YY::g_age = " << YY::g_age << endl;
YY::Person person = YY::Person();
person.run();
return 0;
}
// log:
YY::g_age = 20
Person()
run()
~Person()
思考:下边的代码能通过编译吗
namespace FX {
int g_age;
}
namespace YY {
int g_age;
}
int main() {
using namespace YY;
using namespace FX;
g_age = 20; // 报错:Reference to 'g_age' is ambiguous
return 0;
}
命名空间的嵌套
有个默认的全局命名空间,我们创建的命名空间默认都嵌套在它里面
namespace YY {
namespace XX {
int g_age;
}
};
int g_age;
int main() {
::g_age = 20;
::YY::XX::g_age = 30;
return 0;
}
namespace YY {
namespace XX {
int g_age;
}
};
// 以下的用法都是合法的
int main() {
{
using namespace YY::XX;
g_age = 20;
}
{
using YY::XX::g_age;
g_age = 20;
}
{
YY::XX::g_age = 20;
}
return 0;
}
命名空间的合并
以下2种写法是等价的
namespace YY {
int g_age1;
};
namespace YY {
int g_age2;
}
namespace YY {
int g_age1;
int g_age2;
}
其他编程语言的命名空间
- Java : Package
- Objective-C : 类前缀
继承
- 继承,可以让子类拥有父类的所有成员(变量\函数)
- C++中没有像Java、Objective-C的基类
- Java:java.lang.Object
- Objective-C:NSObject
对象的内存布局
成员访问权限
-
成员访问权限、继承方式有3种
- public:公共的,任何地方都可以访问(struct默认)
- protected:子类内部、当前类内部可以访问
- prvate:私有的,只有当前类内部可以访问(class默认)
-
子类内部访问父类成员的权限,是以下2项中权限最小的那个
- 成员本身的访问权限
- 上一级父类的继承方式
开发中用的最多的继承方式是public,这样能保留父类原来的成员访问权限
访问权限不影响对象的内存布局
初始化列表
- 特点
- 一种便捷的初始化成员变量的方式
- 只能用在构造函数中
- 初始化顺序只跟成员变量的声明顺序有关
- 图片中的2种写法是等价的
struct Student {
int m_age;
int m_height;
Student(int age, int height){
this->m_age = age;
this->m_height = height;
}
};
struct Student {
int m_age;
int m_height;
Student(int age, int height): m_age(age), m_height(height){}
};
思考:
m_age、m_height的值是多少
int myAge(){return 20;};
int myHeight(){return 170;};
struct Student {
int m_age;
int m_height;
Student(int age, int height): m_age(myAge()), m_height(myHeight()){}
};
int main() {
Student s(10,140);
return 0;
}
// 20, 170
struct Student {
int m_age;
int m_height;
// 警告:Field 'm_height' is uninitialized when used here
Student(int age, int height): m_age(m_height), m_height(height){}
};
int main() {
Student s(10,140);
return 0;
}
// 未知、140
构造函数的互相调用
struct Student {
int m_age;
int m_height;
Student():Student(0, 0){};
Student(int age, int height): m_age(age), m_height(height){}
void display(){
cout << m_age << ", "<< m_height << endl;
}
};
int main() {
Student s0;
s0.display();
Student s(10,140);
s.display();
return 0;
}
// log:
0, 0
10, 140
注意:下面的写法是错误的,初始化的是一个临时对象
struct Student {
int m_age;
int m_height;
Student(){
Student(0, 0);
};
Student(int age, int height): m_age(age), m_height(height){}
void display(){
cout << m_age << ", "<< m_height << endl;
}
};
int main() {
Student s0;
s0.display();
Student s(10,140);
s.display();
return 0;
}
// log:
-272632680, 32766
10, 140
初始化列表与默认参数配合使用
- 如果函数声明和实现是分离的
- 初始化列表只能写在函数的实现中
- 默认参数只能写在函数的声明中
struct Student {
int m_age;
int m_height;
Student(int age = 0, int height = 0): m_age(age), m_height(height){}
void display(){
cout << m_age << ", "<< m_height << endl;
}
};
int main() {
Student s1;
Student s2(20);
Student s3(20, 170);
s1.display();
s2.display();
s3.display();
return 0;
}
// log:
0, 0
20, 0
20, 170
父类的构造函数
- 子类的构造函数默认会调用父类的
无参
构造函数 - 如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
- 如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
继承体系下的构造函数示例
struct Person {
int m_age;
Person(): Person(0){};
Person(int age = 0): m_age(age) {}
};
struct Student: public Person {
int m_no;
Student(): Student(0, 0){};
Student(int age, int no): Person(age), m_no(no) {}
void display(){
cout << m_age << ", "<< m_no << endl;
}
};
int main() {
Student s1;
s1.display();
Student s3(2,40);
s3.display();
return 0;
}
// log:
0, 0
2, 40
构造、析构顺序
构造和析构顺序相反
struct Person {
Person(){
cout << "Person()" << endl;
}
~Person(){
cout << "~Person()" << endl;
}
};
struct Student: public Person {
Student(){
cout << "Student()" << endl;
}
~Student(){
cout << "~Student()" << endl;
}
};
int main() {
Student s1;
return 0;
}
// log:
Person()
Student()
~Student()
~Person()
父类指针、子类指针
- 父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
- 子类指针指向父类对象是不安全的