目录
一. explicit(了解)
作用:禁止通过构造函数进行的隐式转换。
解释:下图所示中,出现了给一个对象赋值为10的操作,编译器在其中是做了隐式转换的。
所以,如果为了禁止这种隐式转换,就可以加个explicit关键字。
explicit只能放在构造函数前面,并且构造函数只有一个参数或其他参数有默认值时。
二. 动态对象创建
1. new、delete
在C语言中,可以使用malloc、free在程序运行时进行动态内存分配。
但是在C++中用C语言方式去申请堆区空间,不会调用构造函数,对象释放时也不会调用析构函数。
class Maker
{
public:
Maker()
{
cout << "构造函数" << endl;
}
~Maker()
{
cout << "析构函数" << endl;
}
};
void test01()
{
Maker* m = (Maker*)malloc(sizeof(Maker));
}
C++中使用C语言动态分配函数的缺点:
- 必须先确定对象的长度;
- malloc返回一个void*指针,C++不允许将void*赋值给其他任何指针,必须强转;
- malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功;
- 在使用对象前必须对其进行初始化,然而构造函数并不会显示出调用初始化(因为它是由编译器调用的),用户有可能忘记调用初始化函数。
当创建一个C++对象时会发生两件事:
- 为对象分配内存;
- 调用构造函数来初始化这块内存。
C++中推荐使用new、delete。
C++中解决动态内存分配的方案是:
- 使用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
- delete是先调用析构函数,然后释放内存。
class Maker
{
public:
Maker()
{
cout << "构造函数" << endl;
}
Maker(int a)
{
cout << "有参构造函数" << endl;
}
~Maker()
{
cout << "析构函数" << endl;
}
};
void test01()
{
//用new申请堆区空间,会调用类的构造函数
Maker* m = new Maker;
//释放堆区空间,会调用类的构造函数
delete m;
m = NULL;
Maker* m2 = new Maker(10);
delete m2;
m2 = NULL;
}
输出为:
构造函数
析构函数
有参构造函数
析构函数
2. new和delete用于数组时
(1)一般数组
//创建字符数组
char* pStr = new char[10];
//创建整型数组
int* pInt = new int[10];
//创建整型数组并初始化
int* pInt1 = new int[5]{ 1,2,3,4,5 };
//释放数组内存
delete[] pStr;
delete[] pInt;
delete[] pInt1;
(2)创建对象数组
//创建堆上对象数组必须提供构造函数
Maker* m = new Maker[2]; //会自动调用2次无参构造函数
delete[] m;
//栈聚合初始化,大部分编译器不支持这种写法
Maker* m2 = new Maker[2]{ Maker(10),Maker(20) };
(3)delete void*
可能会出错,因为它不会执行析构函数。
//会调用构造函数
void* m = new Maker;
//如果用void*来接new创建的对象,那么delete时不会调用析构函数
delete m;
在编译阶段,编译器就确定好了函数的调用地址(在new的时候就确定要调用构造函数),但是C++编译器不认识 void*,不知道其指向哪个函数,所以不会调用析构函数。这种编译方式叫静态联编。
三. 静态成员(了解)
类的成员中,用关键字 static 声明为静态的,就是静态成员。
不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
(1)静态成员变量
静态成员变量的生命周期是整个程序,作用域在类内。
静态成员变量必须在类内声明,类外初始化。
class Maker
{
public:
Maker()
{}
public:
static int a;
private:
static int b;
};
int Maker::a = 10;
int Maker::b = 20;
- 静态成员变量属于类,不属于某个对象,是所有对象共享的。
- 在为对象分配空间时不包括静态成员所占空间。
- 静态成员变量可以用类访问,也可以用对象访问。
- 静态成员变量在编译阶段就分配空间,对象还没创建时就已经分配好了空间。
- 静态成员变量也有权限,如果为私有,类外也不可以访问,但是可以在类外进行初始化。
(2)静态成员函数
- 静态成员函数只能访问静态变量,不能访问普通成员变量;
- 静态成员函数的使用和静态成员变量一样;
- 静态成员函数也有访问权限,类外无法访问私有属性的静态成员函数;
- 普通成员函数可以访问静态成员变量,也可以访问非静态成员变量。
class Person{
public:
//普通成员函数可以访问static和non-static成员属性
void func1(int pm)
{
m = pm;
n = pm;
}
//静态成员函数只能访问static成员属性
static void func2(int pm)
{
//m = pm; //err,无法访问
n = pm;
}
private:
static void func3(int pm)
{
//m = pm; //无法访问
n = pm;
}
public:
int m;
static int n;
};
//静态成员属性类外初始化
int Person::n = 0;
int main()
{
//1. 类名直接调用
Person::func2(100);
//2. 通过对象调用
Person p;
p.func2(200);
//3. 静态成员函数也有访问权限
//Person::func3(100); //类外无法访问私有静态成员函数
//Person p1;
//p1.func3(200);
return EXIT_SUCCESS;
}
(3)const 静态成员属性
如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 修饰。
const 修饰的静态成员变量最好在类内部初始化,类外也行。
class M
{
public:
const static int a = 1;
}
(4)单例模式
单例模式是指:一个类只能实例化一个对象。
实现单例模式的思路:
- ① 把无参构造函数和拷贝构造函数私有化;
- ② 定义一个类内的静态成员指针;
- ③ 在类外初始化时,new一个对象;
- ④ 把指针的权限设为私有,然后提供一个静态成员函数让外面获取到这个指针。
单例模式案例:
记录谁用了打印机,并统计打印总次数。
class Printer
{
private:
//1. 把无参构造和拷贝构造私有化
Printer()
{
cnt = 0;
}
Printer(const Printer& p)
{}
public:
static Printer* getPrinter()
{
return p;
}
void printP(string name)
{
cout << name << "打印" << endl;
cnt++;
}
int getCnt()
{
return cnt;
}
private:
int cnt; //记录打印机打印次数
//2. 定义静态成员指针,并把指针权限设为私有
static Printer* p;
};
//3.类外进行初始化,new对象
Printer* Printer::p = new Printer;
void test()
{
Printer* p1 = Printer::getPrinter();
p1->printP("you");
Printer* p2 = Printer::getPrinter();
p2->printP("she");
Printer* p3 = Printer::getPrinter();
cout << "打印次数 = " << p3->getCnt() << endl;
}
输出为:
you打印
she打印
打印次数 = 2
四. C++的对象模型
(1)空类的大小是1.
(2)类的成员中, 成员函数和成员变量是分开存储的。
class Maker
{
public:
void func() //成员函数不占用类的大小
{}
static int a; //静态成员变量不占用类的大小
static void func2() //静态成员函数不占用类的大小
{}
int b; //普通成员变量占用类的大小
};
int Maker::a = 10;
(1)this 指针
根据上边的结论,成员函数不占用类的大小,是单独存储的。那么,如果定义了很多个对象,怎么知道是哪个对象在调用这唯一的成员函数呢?
答:
this指针是C++实现封装的一种机制,它指向被调用的成员函数所属的对象。
this 指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每个成员函数都含有一个系统自动生成的隐含指针 this,用以保存这个对象的地址。因此 this 指针也称为“指向本对象的指针”。
- this 指针并不是对象的一部分,是编译器自动添加的,不会影响sizeof(对象)的结果。
- 成员函数通过 this 指针即可知道操作的是哪个对象的数据。
- this 指针是一种隐含指针,隐含于每个类的非静态成员函数中。
- this 指针无需定义,直接使用即可。
- 静态成员函数内部没有 this 指针,静态成员函数不能操作非静态成员变量。
class Maker
{
public:
//当形参名和成员变量名相同时,用this指针区分
Maker(int id)
{
this->id = id;
}
//在类的非静态成员函数中返回对象本身,可以使用return *this;
Maker& get()
{
return *this;
}
public:
int id;
};
Q1:this 指针指向的空间有没有存储静态成员变量?
答:没有。this 指针指向对象,静态成员变量不存储在对象中。
Q2:this 指针的指向可以改变吗?
答:不可以。this 指针可以理解为:Maker* const this;
(2)常函数、常对象
常函数:
const修饰的成员函数。
用const 修饰的成员函数时,const 修饰 this 指针指向的内存区域,成员函数体不可以修改本类中的任何普通成员变量。
当成员变量类型符前用mutable修饰时例外。
常对象:
在数据类型前加上const,对象成为常对象。
- 常对象可以调用常函数;
- 常对象可以修改mutable修饰的成员变量;
- 常对象不能改变普通成员变量的值;
- 常对象不能调用普通成员函数;
- 普通对象也可以调用常函数。
五. 友元
类的私有成员无法在类的外部(作用域之外)访问,但是,有时候需要在类的外部访问类的私有成员,就可以使用友元函数。
- friend 关键字只出现在声明处;
- 某个类、类成员函数、全局函数都可声明为友元;
- 友元函数不是类的成员函数,不带this指针;
- 友元函数可访问对象任意属性的成员,包括私有属性。
class Person
{
//声明这个全局函数为Person类的友元函数
friend void goodPerson(Person& p);
public:
int id;
private:
int age;
public:
Person()
{
id = 1;
age = 18;
}
};
void goodPerson(Person& p)
{
//本来在类的外部不能访问类的私有成员,当把该全局函数声明为友元函数后,即可访问
cout << "ID = " << p.id << endl;
cout << "AGE = " << p.age << endl;
}
void test()
{
Person p1;
goodPerson(p1);
}
友元类
(1)通过传参访问类的私有成员
class Person
{
//声明HELLO类为Person类的友元类
friend class HELLO;
public:
int id;
private:
int age;
public:
Person()
{
id = 1;
age = 18;
}
};
class HELLO
{
public:
void func(Person& p)
{
cout << "访问:" << p.id << endl;
cout << "访问:" << p.age << endl;
}
};
//通过传参访问类的私有成员
void test()
{
Person p2;
HELLO he;
he.func(p2);
}
(2)通过类内指针来访问类的私有成员
class Person
{
//声明HAHA类为Person类的友元类
friend class HAHA;
public:
int id;
private:
int age;
public:
Person()
{
id = 1;
age = 18;
}
};
class HAHA
{
public:
Person* per;
public:
HAHA()
{
cout << "无参构造" << endl;
per = new Person;
}
void func()
{
cout << "访问:id = " << per->id << endl;
cout << "访问:age = " << per->age << endl;
}
HAHA(const HAHA& ha)
{
cout << "拷贝构造" << endl;
per = new Person;
}
~HAHA()
{
cout << "析构函数" << endl;
if (per != NULL)
{
delete per;
}
}
};
void test01()
{
HAHA h;
h.func();
HAHA h2 = h;
}
输出为:
无参构造
访问:id = 1
访问:age = 18
拷贝构造
析构函数
析构函数
可以看出,在拷贝构造函数中,只进行了申请空间的操作,并没有做赋值操作,最后调用了两次析构函数,分别释放了 h 和 h2。如果不自己定义拷贝构造函数来申请新的空间,则会报错,因为导致了析构函数释放了两次同一空间。
(3)类的成员函数作为另一个类的友元函数
注意:要先声明 func() 函数。
其实可以不用这么复杂,直接把类HAHA声明为类HELLO的友元类,或者把 func() 函数写为全局的也行。
class HELLO;
class HAHA
{
public:
void func(HELLO& he);
};
class HELLO
{
friend void func(HELLO& he);
public:
HELLO()
{
name = "hello world";
age = 1;
}
public:
string name;
private:
int age;
};
void HAHA :: func(HELLO& hei)
{
cout << "访问:name = " << hei.name << endl;
cout << "访问:age = " << hei.age << endl;
}
(4)友元的注意
- 友元关系不能被继承;
- 友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友;
- 友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
六. 防止空指针调用成员函数
在成员函数中加判断 this 指针是否为空的语句。
class Maker
{
public:
Maker()
{
cout << "构造函数" << endl;
a = 0;
}
void printMaker()
{
if (this == NULL)
{
cout << "this为空" << endl;
return;
}
//本来指针m为空,表示this指向空,这里是会出错的。但是前边加了判断this是否为空,即可避免空指针调用
cout << this->a << endl;
}
private:
int a;
};
void test01()
{
Maker* m = NULL;
m->printMaker();
}