类
C++中可以使用struct和class来定义一个类,在C++中,struct和class的区别是struct默认成员权限是public,而class成员权限是private,其它没有区别,为了体现封装性,实际开发中采用class声明一个类,看一个比较完整的C++类的声明:
#include <iostream>
using namespace std;
struct Person {
int m_id;
int m_age;
int m_height;
void display() {
cout << "m_id = " << this->m_id << endl;
cout << "m_age = " << this->m_age << endl;
cout << "m_height = " << this->m_height << endl;
}
};
int main() {
Person person;
person.m_id = 10;
person.m_age = 20;
person.m_height = 30;
return 0;
}
这里声明的是一个栈对象,然后给对象赋值成员变量,这里需要说下this指针,在调用display成员函数的时候,编译器自动会将调用这个函数的对象地址传进去,编译器会帮我们生成this指针保存这个对象地址,有点像是多传了一个参数到成员函数便于访问。这里需要强调的是:类的内存是不包括函数的,函数地址是编译后就固定的,跟对象内存不在一个地方。sizeof(person)得出的大小是12,每个int占四个字节,正由于如此,函数代码能通过this找到对象的成员。结论:this指针指向调用该函数的对象,观察以下代码是否正常输出:
struct Person {
int m_id;
int m_age;
int m_height;
void display() {
cout << "display" << endl;
}
};
int main() {
Person *person = nullptr;
person->display();
return 0;
}
答案是:正常打印函数,如果函数没有调用this访问成员变量程序是不会崩溃的。
堆空间
每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域
- 代码段(代码区):用于存放代码
- 数据段(全局区):用于存放全局变量等
- 栈空间:每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间,自动分配和回收
- 堆空间:需要主动去申请和释放
以上代码声明对象是通过是在栈上声明的,如果是堆对象初始化,代码如下:
Person *per = new Person();
这样申请的内存必须通过析构函数回收堆内存,否则造成内存泄露。
delete per;
堆空间初始化,下面用int类型的指针举例,其它数据类型类似:
int *p1 = (int *)malloc(sizeof(int)); // *p1未初始化
int *p2 = (int *)malloc(sizeof(int));
memset(p2, 0, sizeof(int)); //*p2的每一个字节初始化为0
int *p3 = new int; //*p3未被初始化
int *p4 = new int();//*p4被初始化为0
int *p5 = new int(5);//*p5被初始化为5
int *p6 = new int[3]; //3个元素的数组元素未被初始化
int *p7 = new int[3](); //3个元素的数组被初始化为0
int *p8 = new int[3]{}; //3个元素的数组被初始化为0
int *p9 = new int[3]{5}; //3个元素的首元素被初始化5,其它元素为0
int *p10 = new int[3]{5,4,3}; //3个元素的首元素分别被初始化5、4、3
对象
对象的内存一般分布在三个3个区域:
- 全局变量:全局变量存在于数据段
- 堆空间:动态申请的内存(malloc,new等)
- 栈空间:函数里面的局部变量
Person g_person; //全局对象
int main(int argc, const char * argv[]) {
Person *p = new Person();//堆空间
Person person; //栈空间
}
一般对象的初始化后会调用构造函数,用于对象的初始化,构造函数有以下特点:
- 函数名与类名相同,无返回值(void)都不能写,可以有参数,可以重载,可以有多个构造函数
- 一旦自定义了构造函数,必须用其中一个自定义构造函数去初始化
- malloc分配内存的时候不会调用构造函数,C++中一般用new
- 如果类中没有构造函数,在某些特定情况下编译器才会为类生成一个默认的无参构造函数。
具体请看下面的构造函数注意有参构造和无参构造的函数调用次数,声明对象需要跟函数声明区分开,成员变量用memset比较快进行初始化:
#include <iostream>
using namespace std;
struct Person {
int m_age;
Person() {
cout << "Person()" << endl;
// this->m_age = 0;
memset(this, 0, sizeof(Person));
}
Person(int age) {
cout << "Person(int age)" << endl;
this->m_age = age;
}
};
// 全局区
Person g_person1; // Person()
Person g_person2(); // 这是一个函数声明,函数名叫g_person2,无参,返回值类型是Person
Person g_person3(10); // Person(int age)
int main() {
// 栈空间
Person person1; // Person()
Person person2(); // 函数声明,函数名叫person2,无参,返回值类型是Person
Person person3(20); // Person(int age)
// 堆空间
Person *p1 = new Person; // Person()
Person *p2 = new Person(); // Person()
Person *p3 = new Person(30); // Person(int age)
// 4次无参构造函数
// 3次有参构造函数
getchar();
return 0;
}
C++中有构造函数产生对象,也有析构函数,析构函数负责回收堆空间内存,注意点如下:
- 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作
- 通过malloc分配的对象free的时候不会调用析构函数
- 构造函数、析构函数要声明为public,才能被外界正常使用
- 析构函数一般一个new对应一个delete,在对象中又声明对象的时候要记得释放对象。
#include <iostream>
using namespace std;
struct Car {
int m_price;
Car() {
cout << "Car()" << endl;
}
~Car() {
cout << "~Car()" << endl;
}
};
struct Person {
int m_age; // 4
Car *m_car; // 4
// 初始化工作
Person() {
cout << "Person()" << endl;
this->m_car = new Car();
}
// 内存回收、清理工作(回收Person对象内部申请的堆空间)
~Person() {
cout << "~Person()" << endl;
delete this->m_car;
}
};
void test() {
Person *p = new Person();
delete p;
}
int main() {
test();
return 0;
}
在实际开发中,一个类的声明和实现一般是分开的,比如,在person.h中声明:
#pragma once
// 声明 .h 头文件
class Person {
int m_age;
public:
Person();
~Person();
void setAge(int age);
int getAge();
};
person.cpp中实现如下:
#include "Person.h"
#include <iostream>
using namespace std;
// ::是域运算符
// 实现 .cpp 源文件
Person::Person() {
cout << "Person()" << endl;
}
Person::~Person() {
cout << "~Person()" << endl;
}
void Person::setAge(int age) {
this->m_age = age;
}
int Person::getAge() {
return this->m_age;
}
在函数名前写上类名然后跟一个::才能实现类的里面一些成员函数。在上面一些代码中我们经常看到using name space std类似的声明,这里就牵扯出一个C++经常使用的概念命名空间,可以用来避免命名冲突。命名空间注意事项如下:
- 命名空间不影响内存布局
- 命名空间里面可以嵌套命名空间
- 如果用using声明多个命名空间,外界访问相同的变量会生成二义性而报
- 相同的命名空间可以合并,命名空间最外面是有一个空的命名空间,只是一般省略
namespace MyNameSpace {
int g_age; //全局变量
class Person{
int g_age; //类的成员变量
void test(){} //类的成员函数
}; //声明一个类
void test(){} //类外面的函数
};
int main(int argc, const char * argv[]) {
::MyNameSpace::Person p;
MyNameSpace::test();
}
继承
继承,可以让子类拥有父类的所有成员(变量\函数)
struct Person {
int m_age;
void run() {
cout << "run()" << endl;
}
};
struct Student : Person {
int m_score;
void study() {
cout << "study()" << endl;
}
};
int main {
Student student;
student.m_age = 18;
student.m_score = 100;
student.run();
student.study();
return 0;
}
注意:如果继承父类子类含有同名变量,这个不会覆盖,而是会同时继承下来,所以使用的时候需要制定域名作用符区分,也就是必须制定是父类还是子类。
#include <iostream>
using namespace std;
struct Person
{
int m_age;
void run(){
cout << "person run()" << endl;
}
};
struct Student:Person
{
int m_age;
void run(){
cout << "Student run()" << endl;
}
};
int main(int argc, const char * argv[]) {
Student stu;
stu.Person::m_age = 100; //同名变量必须域名作用符区分赋值变量
stu.Student::m_age = 100;
stu.Person::run(); //同名成员函数必须域名作用符区分赋值变量
stu.run(); //不制定域名作符号,默认是使用子类
return 0;
}
关于对象的内存布局,先是父类成员变量再是子类成员变量,成员函数在代码段,不占用对象内存,仅仅只是把对象地址传给了函数,提供一个类似的接口便于成员函数访问。
访问权限
C++中继承方式一般有3种:
1.public:公共的,任务地方都可以访问,struct默认是public,继承也是public继承
2.protected:保护的,自己和子类可以访问
3.private:私有,只有自己类内部可以访问
关于子类访问父类成员变量是否有权限,取以下2个访问权限最小权限:
1.成员本身的权限
2.上一级父类的继承方式,如果没有继承就忽略
下面举一个关于继承的列子:
class Person {
public:
int m_age;
void run() {
}
};
class Student : public Person {
public:
int m_no;
void study() {
m_age = 10; //可以访问,因为该成员变量是
}
};
class GoodStudent : private Student {
public:
int m_money;
void work() {
m_age = 10; //m_age 本身是public,父类继承是保护继承,可访问
m_no = 100; //m_no 本身是public,父类继承是保护继承,可访问
}
};
class veryGoodStudent : public GoodStudent {
void study_hard() {
//m_age = 100; //m_age 不可访问,因为m_age本身是public的,但是通过GoodStudent私有继承,属性不可访问
m_money = 10; //m_age 本身是public,父类继承是保护继承,可访问
}
};
总结:父类的成员变量或者成员函数是否能够访问与否,看这个成员变量本身的属性以及这个属性以什么样的方式继承过,访问权限取最小访问。
初始化列表
1.一种便捷的初始化成员变量的方式
2.只能用在构造函数中
3.初始化顺序只跟成员变量的声明顺序有关
4.初始化列表的参数可以是变量也可以是函数返回值
//一般的形式
Person(int age,int height):m_age(age),m_height(height){}
int myAge(){ return 100;}
int myHeight() { return 200;}
//参数是函数返回值
Person(int age,int height):m_age(myAge()),m_height(myHeight()){}
初始化列表只跟成员变量的声明有关,如下所示:
//这里会先去初始化m_age,但是m_height此时没有初始化,m_age的值未知
Person(int age,int height):m_height(height),m_age(m_height){}
Person p(100,10); //得到结果m_age = 未知 m_height = 100
我们再来看看构造函数的相互调用,构造函数相互调用可以通过初始化列表调用,在内部调用会创建临时对象。并不指向最外层调用对象。下面这种调用是错误的:
Person(){
// 直接调用构造函数,会创建一个临时对象,传入一个临时的地址值给this指针
Person(0, 0);
}
Person(int age, int height) :m_age(age), m_height(height) { }
构造函数的正确调用如下:
Person() :Person(10, 10) { } //无参构造通过初始化列表调用有参的构造函数
Person(int age, int height) :m_age(age), m_height(height) { }
Person p;
p.display();
初始化列表与默认参数的配合使用,如下所示:
Person(int age = 5, int height = 100) :m_age(age), m_height(height) { }
Person p1; //没有参数,调用使用默认参数
Person p2(1); //age使用1初始化,height使用默认参数
Person p3(3,4); // age = 3 height = 4
请注意,如果类的声明和实现分离的,初始化列表只能写在函数的实现中,而默认参数只能写在函数声明中
struct Person {
int m_age;
int m_height;
Person(int age = 5,int height = 10); //函数声明中
};
Person::Person(int age,int height):m_age(age),m_height(height){} //初始化列表在函数实现中
关于父类与子类的构造函数,有以下几点需要注意的:
子类的构造函数默认会调用父类的无参构造
struct Person {
int m_age;
int m_height;
Person():Person(10,20){
cout << "Person()" << endl;
};
Person(int age,int height):m_age(age),m_height(height){
cout << "Person(int age,int height)" << endl;
}
};
struct Student :public Person
{
int m_score;
//子类如果不显示调用父类构造函数,会默认调用父类的无参构造
Student(int score):m_score(score){}
}
如果子类调用了父类的有参构造函数,则不会再调用无参构造函数。
Person(int age,int height):m_age(age),m_height(height){
cout << "Person(int age,int height)" << endl;
}
//子类如果显示调用父类有参构造函数,则不会调用无惨构造函数
Student(int score):m_score(score),Person(10,20){}
如果父类只有有参构造,没有无参构造,子类必须显示调用父类的有参构造。构造函数与析构函数的顺序刚好相仿,构造函数是先从父类构造,再构造自己,而析构函数刚好相反。