C++基础知识(三)---动态对象、对象模型、静态成员、友元

目录

一. explicit(了解)

 二. 动态对象创建

1. new、delete

2. new和delete用于数组时

(1)一般数组

(2)创建对象数组

(3)delete void*

三. 静态成员(了解)

(1)静态成员变量

(2)静态成员函数

(3)const 静态成员属性

(4)单例模式

 单例模式案例:

四. C++的对象模型

(1)this 指针

(2)常函数、常对象

 五. 友元

友元类

(1)通过传参访问类的私有成员

(2)通过类内指针来访问类的私有成员

(3)类的成员函数作为另一个类的友元函数

(4)友元的注意

六. 防止空指针调用成员函数


一. 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++中推荐使用newdelete

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();
}

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值