一、初始化列表
1.对象成员
对象成员即一个类的对象作为另外一个类的成员。
- 代码演示
c++
class A
{
private:
int a;
public:
A()
{
a = 0;
cout << "A的无参构造被调用" << endl;
}
A(int a1)
{
a = a1;
cout << "A的有参构造被调用:a = " << a << endl;
}
~A()
{
cout << "A的析构函数被调用:a = " << a << endl;
}
};
class B
{
private:
int b;
// 对象成员
A obj_a;
public:
B()
{
b = 0;
cout << "B的无参构造被调用" << endl;
}
B(int a1, int b1)
{
b = b1;
cout << "B的有参构造被调用:b = " << b << endl;
}
~B()
{
cout << "B的析构函数被调用:b = " << b << endl;
}
};
-
运行结果
A的无参构造被调用
B的有参构造被调用:b = 22
B的析构函数被调用:b = 22
A的析构函数被调用:a = 0 -
说明:
- 当类中有对象成员的时候,其构造顺序满足,对象成员先构造,再创建的对象本身构造;
- 析构顺序满足先构造的后析构。
但是,诞生了一个问题,就是这里默认调用的是对象成员的无参构造,怎样才能调用其有参构造呢?
2.对象成员的有参构造
首先先明确,我们是无法通过自身对象的构造函数来调用对象成员的有参构造的,因为对象成员先构造,对象自身后构造,等对象自身构造的时候,对象成员早构造完成了,对象自身根本来不及给对象成员传参,因此就引出了初始化列表。
- 代码演示:还是上面的代码,这里只是在 B 的有参构造后面加上初始化列表
c++
B(int a1, int b1):obj_a(a1)
{
b = b1;
cout << "B的有参构造被调用:b = " << b << endl;
}
-
运行结果
A的有参构造被调用:a = 11
B的有参构造被调用:b = 22
B的析构函数被调用:b = 22
A的析构函数被调用:a = 11 -
说明:
- 可以看到,成功调用了对象成员的有参构造;
- 有参构造的方法,就是在自身对象有参构造函数的后面加上
:成员对象名(参数)
,参数通过创建自身对象的时候传入,对象成员要用哪个参数,参数名就和自身对象有参构造函数的哪个参数同名就行,如B(int a1, int b1):obj_a(a1)
,想用a1的值,就写a1,不需要写类型; - 当调用构造函数的时候,会先执行 :后面的初始化列表,隐式调用对象成员的构造函数,先构造对象成员,再构造自身;
- 当存在多个对象成员的时候,它的构造顺序按照成员对象在代码中的上下顺序构造,即先定义的对象成员先构造。
3. explicit 关键字
我们前面学习构造函数的时候,一个参数的构造函数容易发生隐式转换,可以通过 explicit 防止。
- 代码演示
c++
class A
{
private:
int a;
public:
A()
{
a = 0;
cout << "A的无参构造被调用" << endl;
}
explicit A(int a1)
{
a = a1;
cout << "A的有参构造被调用:a = " << a << endl;
}
~A()
{
cout << "A的析构函数被调用:a = " << a << endl;
}
};
void test11()
{
// A obj = 100; // 报错
A obj(100);
}
-
运行结果
A的有参构造被调用:a = 100
A的析构函数被调用:a = 100 -
说明:
- 当不通过 explicit 修饰的时候
A obj = 100
等价于A obj(100)
,此刻创建对象可以成功; - 但是为了防止隐式转换,通过 explicit 修饰一个参数的拷贝构造,当再使用
A obj = 100
创建对象的时候会报错。
- 当不通过 explicit 修饰的时候
二、动态对象
动态对象,即创建堆区空间的对象,需要动态申请堆区空间,我们在 C语言里使用 malloc 申请堆区空间,使用 free 释放堆区空间,但是在 c++ 里面抛弃了这种方式,使用 new 申请堆区空间,使用 delete 释放堆区空间,为什么呢?
1.malloc 和 free 在 c++ 中的缺陷
- 代码演示
c++
class A
{
public:
int a;
A()
{
a = 0;
cout << "A的无参构造被调用" << endl;
}
explicit A(int a1)
{
a = a1;
cout << "A的有参构造被调用:a = " << a << endl;
}
~A()
{
cout << "A的析构函数被调用:a = " << a << endl;
}
};
void test11()
{
A *p = (A *)malloc(sizeof(A));
p->a = 100;
cout << "a = " << p->a << endl;
free(p);
}
-
运行结果
a = 100
-
说明:
- 从上面的运行结果可以发现, malloc 申请堆区空间,创建对象的时候并没有调用构造函数;
- free 释放堆区空间的时候,并没有调用析构函数,因此 c++ 中才抛弃这种动态内存申请方法,转用 new 和 delete;
- 不过这两种方法的区别仅限于成员变量是指针变量的的时候,对于成员变量为普通的数据类型还是一样的。
2. new 申请堆区空间
2.1 new 和 delete 与基本类型
即通过new 和 delete 为基本类型申请堆区空间
- 代码演示
c++
void test12()
{
// 申请一个 int 堆区空间
int *p1 = new int;
*p1 = 100;
cout << "p1 = " << *p1 << endl;
delete p1;
// 申请一个 int 堆区空间并初始化
int *p2 = new int(200);
cout << "p2 = " << *p2 << endl;
delete p2;
// 申请一个int数组空间
int *array1 = new int[5]{11, 22, 33, 44, 55};
int i = 0;
for (i = 0; i < 5; i++)
{
cout << array1[i] << " ";
}
cout << endl;
delete [] array1;
}
-
运行结果
p1 = 100
p2 = 200
11 22 33 44 55 -
说明:
- 和 malloc 相比,new 申请空间更加智能,不需要传申请空间的大小, new 会根据数据类型自动判断;
- 对普通单个数据初始化的时候,直接在类型后面加(数据值)对数据初始化;
- 对数组的初始化,是在数据后面加上{值1, 值2, 值3...};
- delete 释放数组空间的时候,需要在 delete 后面加上 [] ,告诉编译器这是一个数组,如果不加 [] 只会释放指针指向的元素,即数组的第一个元素。
2.2 new 和 delete 操作对象
new 会先创建空间,再调用构造函数初始化对象,delete 调用析构函数,并释放空间。
- 代码演示
c++
// 省略 calss A 的代码,用上面 1.malloc 和 free 在 c++ 中的缺陷 中的 A 类
void test13()
{
// new 申请堆区空间
A *p1 = new A;
cout << "a = " << p1->a << endl;
delete p1;
// new 申请堆区空间,并初始化
A *p2 = new A(20);
cout << "a = " << p2->a << endl;
delete p2;
}
-
运行结果
A的无参构造被调用
a = 0
A的析构函数被调用:a = 0
A的有参构造被调用:a = 20
a = 20
A的析构函数被调用:a = 20 -
说明:运行结果可以看到,调用了构造函数和析构函数,初始化直接在类名后面跟小括号,填入随机数值即可。
2.3.delete 释放 void*
前面的学习我们知道, void* 代表万能指针,可以指向任意数据类型的指针变量,我们通过 delete 释放 void* 类型的数据时,如果是普通变量,无影响,但是是对象就会有影响。
- 代码演示
c++
class Data
{
private:
int a;
int b;
public:
Data()
{
a = 0;
b = 0;
cout << "Data的无参构造被调用" << endl;
}
Data(int a1)
{
a = a1;
b = 0;
cout << "Data的一个参数构造被调用:a = " << a << endl;
}
Data(int a1, int b1)
{
a = a1;
b = b1;
cout << "Data的两个参数的构造被调用:a = " << a << " b = " << b << endl;
}
~Data()
{
cout << "Data的析构函数被调用:a = " << a << " b = " << b << endl;
}
void showData()
{
cout << "a = " << a << " b = " << b << endl;
}
};
void test16()
{
// 普通变量
void *p1 = new int(10);
delete p1;
cout << "--------------------------" << endl;
// 对象
void *p2 = new Data(11, 22);
delete p2;
cout << "--------------------------" << endl;
}
-
运行结果
A的两个参数的构造被调用:a = 11 b = 22
-
说明:
- 对普通变量无影响,是因为普通变量没有析构函数,只要能释放变量的空间就行了;
- 对于对象,析构函数未调用,因为 delete 释放变量看的是定义的时候的数据类型,而这里定义的时候数据类型是万能指针, delete 不知道自己释放的是对象空间,所以就只是将空间释放了,没有调用析构函数。
3.对象数组
3.1 对象数组定义和初始化
对象数组:本质上是数组,只是数组中每个元素是对象。
- 代码演示
c++
// 省略类的代码,还是使用上面的 class Data
void test14()
{
// 创建一个对象数组
Data array1[5];
cout << "-------------分割线--------------" << endl;
// 不建议这种初始化方法,因为这里是每个元素发生了构造函数的隐式转换
// Data array2[5] = {11, 22, 33, 44, 55};
// 正常推荐的写法
Data array2[5] = {Data(11, 22), Data(33, 44), Data(55, 66),
Data(77, 88), Data(99, 00)};
// 遍历数组
int i = 0;
for (i = 0; i < 5; i++)
{
array2[i].showData();
}
}
-
运行结果
Data的无参构造被调用
Data的无参构造被调用
Data的无参构造被调用
Data的无参构造被调用
Data的无参构造被调用
-------------分割线--------------
Data的两个参数的构造被调用:a = 11 b = 22
Data的两个参数的构造被调用:a = 33 b = 44
Data的两个参数的构造被调用:a = 55 b = 66
Data的两个参数的构造被调用:a = 77 b = 88
Data的两个参数的构造被调用:a = 99 b = 0
a = 11 b = 22
a = 33 b = 44
a = 55 b = 66
a = 77 b = 88
a = 99 b = 0
Data的析构函数被调用:a = 99 b = 0
Data的析构函数被调用:a = 77 b = 88
Data的析构函数被调用:a = 55 b = 66
Data的析构函数被调用:a = 33 b = 44
Data的析构函数被调用:a = 11 b = 22
Data的析构函数被调用:a = 0 b = 0
Data的析构函数被调用:a = 0 b = 0
Data的析构函数被调用:a = 0 b = 0
Data的析构函数被调用:a = 0 b = 0
Data的析构函数被调用:a = 0 b = 0 -
说明:
- 定义对象数组未初始化,默认调用的是无参构造;
- 给对象数组初始化的时候,不允许直接在后面等号跟大括号填值,那样相当于分别对数组中每个对象后面等号赋值,每个对象都发生了构造函数的隐式转换;
- 对对象数组赋值,得遵循规定的格式:
Data array2[2] = {Data(11, 22), Data(33, 44)}
,相当于对每个对象显示构造。
3.2 new 和 delete 操作对象数组
即将对象数组存放在手动申请的堆区空间,通过new 和 delete 创建管理和资源释放。
- 代码演示
c++
// 省略类的代码,还是使用上面的 class Data
void test15()
{
// 为对象数组申请堆区空间并初始化
Data *array1 = new Data[5]{Data(11, 22), Data(33, 44), Data(55, 66),
Data(77, 88), Data(99, 00)};
// 遍历数组
int i = 0;
for (i = 0; i < 5; i++)
{
array1[i].showData();
}
// 释放堆区空间
delete [] array1;
}
-
运行结果
Data的两个参数的构造被调用:a = 11 b = 22
Data的两个参数的构造被调用:a = 33 b = 44
Data的两个参数的构造被调用:a = 55 b = 66
Data的两个参数的构造被调用:a = 77 b = 88
Data的两个参数的构造被调用:a = 99 b = 0
a = 11 b = 22
a = 33 b = 44
a = 55 b = 66
a = 77 b = 88
a = 99 b = 0
Data的析构函数被调用:a = 99 b = 0
Data的析构函数被调用:a = 77 b = 88
Data的析构函数被调用:a = 55 b = 66
Data的析构函数被调用:a = 33 b = 44
Data的析构函数被调用:a = 11 b = 22 -
说明:因为是数组,释放空间同样要加 [] ,如果不加默认释放的是第一个对象,如果加了释放所有对象,而且同样遵循先构造的后析构。
三、类的成员
1.指针成员
如果类中有指针成员,必须定义拷贝构造函数(深拷贝)和析构函数。
- 代码演示
c++
class Person
{
private:
int num;
char *name;
float score;
public:
Person()
{
num = 0;
name = NULL;
score = 0.0f;
cout << "Person 无参构造" << endl;
}
Person(int num1, char *name1, float score1)
{
num = num1;
name = new char[strlen(name1) + 1];
strcpy(name, name1);
score = score1;
cout << "Person 有参构造" << endl;
}
Person(const Person &obj)
{
num = obj.num;
name = new char[strlen(obj.name) + 1];
strcpy(name, obj.name);
score = obj.score;
cout << "拷贝构造函数被调用" << endl;
}
~Person()
{
if (NULL != name)
{
delete [] name;
name = NULL;
}
cout <<"析构函数被调用" << endl;
}
void showPerson()
{
cout << "num = " << num << " name = " << name << " score = " << score << endl;
}
};
void test17()
{
Person jack(101, "jack", 88.5);
Person rose = jack;
rose.showPerson();
}
-
运行结果
Person 有参构造
拷贝构造函数被调用
num = 101 name = jack score = 88.5
析构函数被调用
析构函数被调用 -
说明:
- 当对象有指针成员的时候,必须调用拷贝构造,且是深拷贝。如果我们创建新对象时对其等号赋值,默认是浅拷贝,相当于只是将对象的成员拷贝了一份,如果是指针成员,只是拷贝了指针成员保存的地址,这样两个对象的指针成员都指向了同一个内存空间,使用结束,调用析构函数,会导致空间重复释放的问题;
- 拷贝构造是为指针成员开辟了一个新的堆区空间,然后将旧的对象的指针成员指向的空间的值拷贝过来,这样,每个对象的指针成员都指向各自的堆区空间,只是里面保存的内容一样,释放也是释放各自的;
- 有指针成员必须调用析构函数,因为其它栈区空间的成员变量,进程结束会自动释放,但指针成员指向的堆区空间需要手动 delete 释放,析构函数就是为了调用 delete 释放指针成员指向的堆区空间的;
- 我们在前面初始化变量的时候,如果没为指针成员赋值,应将其指向 NULL ,如果不指向NULL,那么指针成员就是个野指针,在析构函数内条件判断指针成员不指向 NULL ,就会调用 delete 释放野指针,结果发生段错误。
2.静态成员
当类中的成员被 static 修饰时,称为静态成员,分为:静态成员数据、静态成员函数。
2.1静态成员数据
- 代码演示
c++
class Data
{
public:
// 普通成员变量
int a;
// 静态成员变量
static int b;
};
// 静态成员变量类外初始化
int Data::b = 100;
void test18()
{
cout << sizeof(Data) << endl;
cout << "b = " << Data::b << endl;
Data obj1;
cout << "b = " << obj1.b << endl;
Data obj2;
obj2.b =200;
cout << "b = " << obj1.b << " " << Data::b << endl;
}
-
运行结果
4
b = 100
b = 100
b = 200 200 -
说明:
- 学过 python 兄弟应该知道,这里的静态成员变量,相当于 python 中的类属性;
- 上面的演示结果,类的大小占4个字节,这4个字节来源于普通成员变量,因为类的大小值的是创建对象的时候开辟的空间的大小,因此,可见静态成员变量并不是属于对象空间的,而是属于类的;
- 静态成员变量要在类中声明,类外初始化,因为静态成员不属于对象空间,因此不能通过创建对象初始化。同时,类只是一个模板,并不占用空间,因此语法规定,类中不能初始化数据,使用智能在类外初始化了;
- 静态成员变量,既可以通过对象访问,也可以通过类名直接访问(不管有没有定义对象都能直接通过类名访问),且数据在类和不同对象间是共享的,即用其中一个对象或者类名修改静态成员变量的值,通过其它对象去访问值都发生了改变。
2.2 静态成员函数
上面的案例中,可以看到静态成员变量设置的权限是 public ,但如果设置 private 私有属性那就不能直接在类外访问静态成员变量了,需要通过公有的成员方法来访问。
但问题又来了,见下面演示:
c++
class Data
{
private:
// 普通成员变量
int a;
// 静态成员变量
static int b;
public:
int getB()
{
return b;
}
};
// 静态成员变量类外初始化
int Data::b = 100;
void test19()
{
Data::getB(); // error: cannot call member function 'int Data::getB()' without object
}
- 说明:前面我们说,即使没有定义对象的时候我们也可以通过类名直接访问静态成员变量,但是现在数据私有了,我们需要通过公有成员函数去访问静态成员变量,但是直接通过类名调用成员函数,会报错:不能在没有实例化对象的时候直接通过类名调用成员方法。为了解决这个问题,引出了静态成员函数:
c++
class Data
{
private:
// 普通成员变量
int a;
// 静态成员变量
static int b;
public:
static int getB()
{
// a = 10; // 报错
return b;
}
};
// 静态成员变量类外初始化
int Data::b = 100;
void test19()
{
cout << Data::getB() << endl;
Data obj1;
cout << obj1.getB() << endl;
}
-
运行结果
100
100 -
说明:
- 定义静态成员变量,只需要在成员函数前面加 static 修饰;
- 静态成员函数里面不能操作普通成员变量,因为普通成员变量是在创建对象以后才会定义,但静态成员函数是为操作私有静态成员变量的。静态成员变量在定义类的时候就存在了,普通成员变量和它之间存在时间上的冲突,用静态成员函数去操作普通成员变量就相当于让它去操作一个未定义的变量,会报错;
- 静态成员函数既能通过类名访问,又能通过对象访问。
2.3静态成员的用法
2.3.1用于统计类实例化对象的个数
- 代码演示
c++
class Data
{
private:
static int count;
public:
Data()
{
count++;
}
Data(const Data &obj)
{
count++;
}
~Data()
{
count--;
}
static int showCount()
{
return count;
}
};
int Data::count = 0;
void test25()
{
Data obj1;
Data obj2 = obj1;
{
Data obj3;
Data obj4;
cout << Data::showCount() << endl;
}
cout << Data::showCount() << endl;
}
-
运行结果
4
2
2.3.2单例模式
单例模式:即该类只能实例化一个对象。
- 代码演示
c++
class Singleton {
//1、构造函数私有化 保证在类外无法实例化对象
private:
Singleton() {}
Singleton(const Singleton &ob) {
*this = ob;
}
~Singleton() {}//防止类外释放唯一地址
private:
//2、定义一个静态的指针变量 用来保存唯一对象的地址
static Singleton *p;
public:
//4、通过静态成员函数 获取唯一的实例地址
static Singleton *getSingleton() {
return p;
}
//5、单例模式的核心任务
void doWork(char *str) {
cout << "打印了:" << str << endl;
}
};
//3、实例化唯一的对象
Singleton *Singleton::p = new Singleton;
void test26() {
//先获取唯一实例地址
Singleton *p1 = Singleton::getSingleton();
p1->doWork("入职证明 1");
p1->doWork("离职证明 1");
p1->doWork("薪资流水 1");
p1->doWork("体检证明 1");
Singleton *p2 = Singleton::getSingleton();
p2->doWork("入职证明 2");
p2->doWork("离职证明 2");
p2->doWork("薪资流水 2");
p2->doWork("体检证明 2");
}
-
运行结果
打印了:入职证明 1
打印了:离职证明 1
打印了:薪资流水 1
打印了:体检证明 1
打印了:入职证明 2
打印了:离职证明 2
打印了:薪资流水 2
打印了:体检证明 2
3. const 修饰的成员函数
const 修饰的成员函数,函数内部不能对成员变量的值进行写操作,mutable 修饰的成员变量除外。
- 代码演示
c++
class Data
{
private:
int a;
mutable int b;
public:
void showData() const
{
// a = 100; // error: assignment of member 'Data::a' in read-only object
b = 200;
cout << "a = " << a << " b = " << b << endl;
}
};
void test24()
{
Data obj;
obj.showData();
}
-
运行结果
a = 0 b = 200
-
说明:
-
const 加在函数名() 的后面,该函数内部再修改成员变量会报错,提示该成员变量为只读,但是 mutable 修饰的成员变量依旧可以对成员变量写操作;
-
这里的 const 修饰的本质上是 *this,后面会讲到,this 指针指向调用这个函数的对象本身。
4.类的大小
类的大小:一般指类实例化对象时,对象占用的空间大小。
成员函数、静态成员函数、静态成员变量都不占用类的空间大小。
因为类是从结构体发展而来的,因此类的空间大小遵循结构体空间的内存排布规律。
- 代码演示
c++
class Data
{
private:
int a;
short b;
char c;
static int d;
public:
int getA()
{
return a;
}
static int getD()
{
return d;
}
};
// 静态成员变量类外初始化
int Data::d = 100;
void test20()
{
cout << sizeof(Data) << endl;
}
-
运行结果
8
-
说明:
- 运行结果为 8 字节,符合上面说的,类的空间大小和结构体的空间大小计算方法一样,只是类的空间大小不计算:静态成员变量、成员函数和静态成员函数的大小;
- 在类中,普通成员变量是各个对象独有的数据,而静态成员变量、成员函数和静态成员函数是对象共有的。
四、 this 指针
1. this 指针的概述
我们在创建多个对象以后,通过对象名去访问对象的成员数据的时候,怎么就知道访问的是哪个变量,而不会访问到别的对象的同名成员数据呢?
- 代码演示
c++
class Data
{
private:
int a;
public:
Data()
{
a = 0;
}
Data(int a1):a(a1){}
void showData()
{
cout << "a = " << a << endl;
}
};
void test21()
{
Data obj1(11);
obj1.showData();
Data obj2(22);
obj2.showData();
}
-
运行结果
a = 11
a = 22
上面的代码演示,obj1.showData()
为什么就知道是获取 obj1 对象的成员变量呢,Data obj2(22)
为什么就知道是获取 obj2 对象的成员变量呢,它们的成员变量名都相同,为什么不会混淆?
这就与接下来要将的 this 指针有关:
因为代码其实为我们简化了一部分内容,其背后的原理:
我们在调用成员函数取成员变量的值的时候,会创建一个 this 指针
void showData() 相当于 void showData(Data *this)
这个 this 指针保存了调用这个函数的对象的地址,如:
obj1.showData() 相当于 obj1.showData(&obj1)
函数内部:
cout << "a = " << a << endl 相当于 cout << "a = " << this -> a << endl
相当于:cout << "a = " << obj1.a << endl
因此:通过哪个对象调用的函数,就会操作该对象的成员变量
- this 指针的目的:保存调用成员函数的对象的地址,静态成员函数没有 this 指针,因为静态成员函数是访问静态成员变量的,静态成员变量是各个对象公有的,都可以访问的,因此没必要通过 this 指针标识是属于哪个对象的成员变量。
2.this 指针的用法
2.1函数的形参与成员名同名
我们前面给对象初始化的时候,构造函数的形参名都和成员名不相同,如果相同,会初始化失败:
c++
class Data
{
private:
int a;
public:
Data()
{
a = 0;
}
Data(int a)
{
a = a;
}
void showData()
{
cout << "a = " << a << endl;
}
};
void test22()
{
Data obj(11);
obj.showData(); // a = 随机值
}
- 可以看到,函数的形参名都和成员名相同时,初始化失败。由于就近原则,这里的变量 a 是函数调用时创建的临时局部变量 a ,这里是将 值赋值给了临时变量,并没有赋值给成员变量。为了解决这个问题,需要指明构造函数里的被赋值的 a 是成员变量 a,于是就用到了 this 指针:
c++
class Data
{
private:
int a;
public:
Data()
{
a = 0;
}
Data(int a)
{
this->a = a;
}
void showData()
{
cout << "a = " << a << endl;
}
};
void test22()
{
Data obj(11);
obj.showData(); // a = 11
}
2.2成员函数返回 *this 完成链式操作
前面我们学习过通过引用作为返回值,首先链式操作,这里通过调用函数的对象本身作为返回值,完成链式操作。
- 代码演示
c++
class Print
{
public:
Print& print(char *str)
{
cout << str << " ";
return *this;
}
};
void test23()
{
Print obj;
obj.print("hello").print("world").print("hello").print("friend");
// 或者直接通过匿名对象调用
cout << endl;
Print().print("hello").print("world").print("hello").print("friend");
}
-
运行结果
hello world hello friend
hello world hello friend -
说明:和前面,结构体中的成员函数将结构体变量的引用返回完成链式操作差不多,只不过之前的函数的形参每次都需要传结构体变量,而在类的成员函数中,本身就有 this 指针指向调用该函数的对象本身,因此,相比之前通过结构体完成链式操作,通过对象更加方便。