目录
一、类
C++ 中可以使用 struct、class 来定义一个类。struct 的默认成员权限是 public,class 的默认成员权限是 private。除此之外,二者基本无差别。struct 中同样可以存在成员函数。
struct Person {
//private:
// 成员变量
int age;
// 成员函数
void run() {
cout << " age is " <<this -> age << endl;
}
}
不同于OC,C++ 中的对象既可以存在栈区,也可以存在堆区。存在于栈空间的对象称为自动 (auto) 变量。
// 在栈空间分配内存给person对象
// 这个person对象的内存会自动回收,不用开发人员去管理
Person person;//调用此行代码,此时已经初始化完毕
person.age = 20;
person.run();
二、对象的内存布局和 this
Person person;
person.age = 20;
person.run();
cout << sizeof(person) << endl;
上述代码打印结果为 4 ,4 仅表示成员变量所占据的内存空间。成员函数不存在对象内存空间中,原因在于成员函数是不同对象公共使用的,如果每个对象的内存空间都拷贝一份会造成不必要的浪费。实际情况,对象在调用成员函数的时候,会自动传入当前对象的内存地址,也即 this
,this
是指向当前对象的指针。注意不能使用this.age
访问成员变量,this
是指针,必须使用 this -> age
形式。这点可以结合汇编知识猜测到,因为混编代码中函数的调用形式为call + 函数地址
。
// 成员函数
void run() {
//编译器允许省略 this ->
cout << "age is " << this -> age << endl;
}
注意:C++ 中不存在方法缓存一说,OC 中之所以存在方法缓存主要是因为有类对象的存在。
三、堆空间(申请、释放、清零)
堆空间的申请和释放可以通过以下两种形式:
- malloc | free
- new | delete
int size = sizeof(int);
// 申请4个字节的堆空间内存
int *p = (int*)malloc(size);
// 从p开始的4个字节, 每个字节都存放0
memset(p, 0, size);
//将10放到内存空间中
*p = 10;
//申请4个字节空间,内部存放 char 类型
*char *p = (char *)malloc(4);
*p = 1;
*(p + 1) = 2;
*(p + 2) = 3;
*(p + 3) = 4;
// 没有初始化
int *p1 = new int;
// 初始化为0
int *p2 = new int();
// 初始化为5
int *p3 = new int(5);
// 没有初始化
int *p4 = new int[3];
// 全部元素初始化为0
int *p5 = new int[3]();
// 全部元素初始化为0
int *p6 = new int[3]{};
// 首元素初始化为5,其他元素初始化为0
int *p7 = new int[3]{ 5 };delete p1;
struct Person {
int m_age;
};
// 堆空间
Person *p = new Person();
p->m_age = 20;
delete p;
四、对象的内存位置(全局、栈、堆)
C++ 中的对象可以存在全局区、栈空间或堆空间。
struct Person {
int m_age;
};
// 全局区
Person g_person;
int main() {
// 栈空间
Person person;
// 堆空间,相比于malloc、free,更推荐此种方式
Person *p = new Person();
p->m_age = 20;
delete p;
// 堆空间
Person *p2 = (Person *) malloc(sizeof(Person));
free(p2);
return 0;
}
五、构造函数
构造函数,也叫构造器,在对象创建的时候自动调用。主要有以下特点:
- 函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数。
- 注意一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象。
- 通过 malloc 创建的对象不会调用构造函数,所以一般建议使用 new 创建对象。
- 要声明为
public
,才能被外界正常使用。
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;
}
};
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)
return 0;
}
六、成员变量初始化方式
变量的初始化有两种形式,一种是在构造函数实现,另一种是在没有构造函数的情况下 new
创建对象的时候添加小括号。
方式一:
struct Person {
int m_age;
Person() {
cout << "Person()" << endl;
//以下两种形式都可以初始化成员变量,更推荐第二种。
// this->m_age = 0;
memset(this, 0, sizeof(Person));
}
};
方式二:
// 全局区(成员变量初始化为0)
Person g_person;
int main() {
// 栈空间(成员变量没有初始化)
Person person;
// 堆空间
Person *p1 = new Person; // 成员变量没有初始化
Person *p2 = new Person(); // 成员变量有初始化
return 0;
}
七、析构函数(Destructor)
析构函数,也叫析构器,在对象销毁的时候自动调用。有以下特点:
- 函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数。
- 通过malloc分配的对象free的时候不会调用构造函数。
- 和构造函数一样,要声明为
public
,才能被外界正常使用。
class Person {
int m_age;
public:
// 对象创建完毕的时候调用
Person() {
cout << "Person()" << endl;
this->m_age = 0;
}
Person(int age) {
cout << "Person(int age)" << endl;
this->m_age = age;
}
// 对象销毁(内存被回收)的时候调用析构函数
~Person() {
cout << "~Person()" << endl;
}
};
八、对象内部内存管理(对象的成员变量为对象)
当对象内部的成员变量为对象时,对象销毁的时候,成员变量对象也会被销毁。但当对象内部的成员变量为对象指针时,可以在对象内部申请堆空间,并由对象内部回收堆空间。
struct Car {
int m_price;
Car() {
cout << "Car()" << endl;
}
~Car() {
cout << "~Car()" << endl;
}
};
成员变量为对象时,构造函数和析构函数都无需做额外处理:
struct Person {
int m_age;
Car m_car;
Person() {
cout << "Person()" << endl;
}
~Person() {
cout << "~Person()" << endl;
}
};
成员变量为对象指针时,构造函数和析构函数都要做处理:
struct Person {
int m_age; //4
Car *m_car; //4
Person() {
cout << "Person()" << endl;
this->m_car = new Car();
//注意不能这样写,因为此时Car对象在栈区,出了构造函数栈区被回收,此时指针指向栈区很危险。
/*Car car;
this->m_car = &car;*/
}
// 内存回收、清理工作(回收Person对象内部申请的堆空间)
~Person() {
cout << "~Person()" << endl;
delete this->m_car;
}
};
使用:
Person *p = new Person();
delete p;
九、方法声明和实现分离
Person.h 文件中的声明:
class Person {
int m_age;
public:
Person();
~Person();
void setAge(int age);
int getAge();
};
Person.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;
}
十、命名空间
命名空间主要用于避免命名冲突。Java 中的命名空间是所谓的 package,OC 中没有命名空间,一般可以用类前缀作区分。
namespace ZW {
class Car {
public:
Car();
~Car();
};
}
int main() {
ZW::Car car;
return 0;
}
命名空间可以嵌套。
namespace AA {
namespace BB {
int g_no;
class Person {
};
void test() {
}
}
}
int main() {
using namespace AA;
BB::g_no = 30;
using namespace AA::BB;
g_no = 10;
return 0;
}
十一、继承以及内存布局
OC 中所有的对象都继承自 NSObject,Java 中所有的对象都继承自 java.lang.object 。但 C++ 不同于 OC 和 Java,C++ 没有最终的基类。
struct Person {
int m_age;
};
struct Student : Person {
int m_no;
};
struct GoodStudent : Student {
int m_money;
};
int main(int argc, const char * argv[]) {
Person person;// 4
Student stu;// 8
GoodStudent gs;// 12
gs.m_age = 20;
gs.m_no = 1;
gs.m_money = 666;
cout << sizeof(Person) << endl;//4
cout << sizeof(Student) << endl;//8
cout << sizeof(GoodStudent) << endl;//12
return 0;
}
下图是 GoodStudent 的内存布局。其中,父类的成员变量在前,子类的成员变量在后。
十二、初始化列表和默认参数
初始化列表
初始化列表一种便捷的初始化成员变量的方式,只能用在构造函数中,初始化顺序只跟成员变量的声明顺序有关。下面代码段中注释的代码可以用一种更简洁的方式表达。
struct Person {
int m_age;
int m_height;
Person(int age, int height) :m_age(age), m_height(height) {
}
/*Person(int age, int height) {
cout << "Person(int age, int height) " << this << endl;
this->m_age = age;
this->m_height = height;
}*/
默认参数
默认参数只能写在函数的声明中,构造函数的初始化列表只能写在实现中。
//类声明
class Person {
int m_age;
int m_height;
public:
// 默认参数只能写在函数的声明中
Person(int age = 0, int height = 0);
};
//实现
// 构造函数的初始化列表只能写在实现中
Person::Person(int age, int height) :m_age(age), m_height(height) {
}
int main() {
Person person;
Person person2(10);
Person person3(20, 180);
return 0;
}
十三、父类的构造函数
- 1、子类的构造函数默认会调用父类的无参构造函数。父类的构造函数先于子类的构造函数调用。
- 2、如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数。
- 3、如果父类没有无参构造函数,子类的构造函数必须显式调用父类的有参构造函数。
class Person {
int m_age;
public:
Person(int age) :m_age(age) {
cout << "Person(int age)" << endl;
}
};
class Student : public Person {
int m_score;
public:
Student() :Person(0) {
}
};
十四、虚函数
虚函数是被 virtual
修饰的成员函数,C++中的多态通过虚函数(virtual function)来实现,只要在父类中声明为虚函数,子类中重写的函数也自动变为虚函数。C++ 中的多态不像 OC 那样智能,如果不使用 virtual
关键字,方法调用者只会根据指针类型去寻找方法并调用,而不是根据对象类型去寻找方法。所以 C++ 若想实现多态,一般要借助 virtual
关键字。
class Animal {
public:
int m_age;
virtual void speak() {
cout << "Animal::speak()" << endl;
}
virtual void run() {
cout << "Animal::run()" << endl;
}
};
class Cat : public Animal {
public:
int m_life;
Cat() :m_life(0) {}
void speak() {
cout << "Cat::speak()" << endl;
}
void run() {
cout << "Cat::run()" << endl;
}
};
Animal *cat1 = new Cat();
cat1->speak();
cat1->run();
十六、虚表
虚函数的实现原理是虚表(也称为虚函数表),虚表里面存储着最终需要调用的虚函数地址。
在没有虚函数的情况下,对象的内存空间为成员变量占据的空间大小。virtual
关键字修饰之后,对象内存会增加 4 个字节,主要用于存储虚表地址,虚表内存中存储着虚函数地址(如果子类没有重写父类虚函数,虚表中存储父类的虚函数地址)。如果对象内存中存在虚表地址,会优先调用根据虚表中的函数地址调用对应的函数,以保证调用对应类型对象的方法;如果不存在虚表,就只能依据编译器特性根据指针类型对调用对应的方法。注意:每创建一个 Cat 对象内部都包含虚表地址,但是这些 Cat 对象的虚表地址指向同一张虚表内存空间。
十七、纯虚函数
纯虚函数是指没有函数实现且初始化为 0 的虚函数,主要用来定义接口规范。纯虚函数对应的类也被称为抽象类,抽象类不可以实例化,只有被继承使用才有意义。抽象类也可以包含非纯虚函数,如果父类是抽象类,子类没有完全实现纯虚函数,那么这个子类依然是抽象类。
//这是一个纯虚函数对应的类,它是一个抽象类,不能直接拿来初始化。
class Animal {
public:
virtual void speak() = 0;//纯虚函数
virtual void run() = 0;//纯虚函数
void test{
}
};
class Cat : public Animal {
public:
void run() {
}
};
十八、多继承
C++ 允许一个类可以有多个父类,但一般不建议使用,因为会增加程序设计复杂度。
class Student {
public:
int m_score;
void study() {
cout << "Student::study() - score = " << m_score << endl;
}
void eat() {
cout << "Student::eat()" << endl;
}
};
class Worker {
public:
int m_salary;
void work() {
cout << "Worker::work() - salary = " << m_salary << endl;
}
void eat() {
cout << "Worker::eat()" << endl;
}
};
class Undergraduate : public Student, public Worker {
public:
int m_grade;
void play() {
cout << "Undergraduate::play() - grade = " << m_grade << endl;
}
void eat() {
cout << "Undergraduate::eat()" << endl;
}
};
多继承同名函数问题
多继承的同名函数主要通过命名空间来做区分。
Undergraduate ug;
ug.eat();
ug.Worker::eat();
ug.Student::eat();
十九、菱形继承和虚继承
菱形继承
class Person {
public:
int m_age;
};
class Student : public Person {
public:
int m_score;
};
class Worker : public Person {
public:
int m_salary;
};
class Undergraduate : public Student, public Worker {
public:
int m_grade;
};
菱形继承中最底部子类从基类继承的成员变量冗余、重复,所以最底部子类无法访问基类的成员,有二义性。即Undergraduate
对象无法访问基类 Person
的成员变量 m_age
。
虚继承
虚继承可以用来解决菱形继承问题。
class Student : virtual public Person {
public:
int m_score;
};
class Worker : virtual public Person {
public:
int m_salary;
};
二十、static
静态成员变量
被 static
修饰的成员变量称为静态成员变量,可以通过对象(对象.静态成员)、对象指针(对象指针->静态成员)、类访问(类名::静态成员)。存储在全局区,类似于全局变量,整个程序运行过程中只有一份内存。同全局变量最大的区别在于,静态成员变量限制了作用域,仅相关类可以直接使用。使用时需注意必须初始化,且必须在类外面初始化。
class Person {
public:
static int ms_count;
};
//必须在类外面初始化
int Person::ms_count = 0;
int main() {
Person::ms_count = 20;
return 0;
}
静态成员函数
被 static
修饰的成员函数称为静态成员函数,存在于代码区。内部不能使用 this
指针,this
指针只能用在非静态成员函数内部。内部不能访问非静态成员变量\函数,只能访问静态成员变量\函数。不能是虚函数,虚函数只能是非静态成员函数。
class Car {
public:
static int m_price;
static void run() {
cout << "run()" << endl;
}
};
静态成员应用(单例)
class Rocket {
public:
// C++:静态成员函数
// Java、OC:类方法
//提供一个公共的返回单例对象的静态成员函数
static Rocket * sharedRocket() {
// API p_thread
if (ms_rocket == NULL) {
ms_rocket = new Rocket();
}
return ms_rocket;
}
static void deleteRocket() {
static Rocket *ms_rocket;
if (ms_rocket == NULL) return;
delete ms_rocket;
ms_rocket = NULL;
}
private:
//使用 static 保证只有一个,私有化禁止外部访问
static Rocket *ms_rocket;
//禁止外部创建
Rocket() {
cout << "Rocket()" << endl;
}
~Rocket() {
cout << "~Rocket()" << endl;
}
};
Rocket *Rocket::ms_rocket = NULL;
int main() {
Rocket *p1 = Rocket::sharedRocket();
// ......
Rocket::deleteRocket();
return 0;
}