C++学习周报(2023.12.17~2023.12.23)

文章详细讲解了C++中的类和对象存储机制,this指针的用法,解决名称冲突的方法,常函数和常对象的概念,友元的三种实现方式,以及运算符重载,如加号、左移、递增和赋值运算符的重载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

2.8类和对象的存储

2.9 this指针

解决名称冲突

返回对象本身用*this

2.10空指针访问成员函数

2.11 const修饰成员函数

常函数:

常对象:

3.友元

3.1一点概述

3.2三种实现

全局函数作友元

类作友元

成员函数作友元

4.运算符重载

4.1加号运算符重载

4.2左移运算符重载

4.3递增运算符重载

4.4赋值运算符重载

4.5函数调用运算符重载


接上篇

2.8类和对象的存储

空对象占用内存空间为:1

C++编译器为了区分不同的空对象,需要给对象分配内存空间,所以在节省内存的前提下给空对象分配一个字节空间。

我们可以运行下面这段代码来测试

#include <iostream>
using namespace std;

class Person
{
	
};

int main(){
	Person p;
	cout << sizeof(p) << endl;
	
	return 0;
}

运行结果应该是 1 ,至少我的编译器是这样。(当然末尾还有换行,考试重灾区哈哈)

那么如果类里面定义了一个int类型的成员变量呢?这时对象占用的内存是多大?

4,5,8是不同人想的答案,但正确的只有一个 4

当对象中包含成员变量时,编译器为其分配内存空间,这时便不再需要那一个字节内存

可以理解为编译器保证对象有自己独特的内存地址

为了进一步探究成员变量和成员函数在对象中占用的内存,我们可以依次输入下列语句进行测试

int a;
	
static int b;
	
void fun1(){ };
	
static void fun2(){ };

在逐个运行后我们可以知道结果并得出结论

#include <iostream>
using namespace std;

class Person
{
	int a; // 非静态成员变量  属于类的对象上 4
	
	static int b; // 静态成员变量   不属于类的对象上 4
	
	void fun1(){ }; // 非静态成员函数  不属于类的对象上 4
	
	static void fun2(){ }; // 静态成员函数  不属于类的对象上 4
};

int main(){
	Person p;
	cout << sizeof(p) << endl;
	
	return 0;
}

在经过测试后我们可以发现,只有使用int a 时该程序输出改变为 4 ,也就是说只有非静态成员变量属于类的对象上,其他成员均不属于,也不会对对象的内存占用造成影响。

一句话:成员变量和成员函数是分开存储的

2.9 this指针

this指针主要有两个用途

  1. 解决名称冲突
  2. 在类的非静态成员函数中返回对象本身,可使用return  *this

解决名称冲突

this指针指向 被调用的成员函数 所属的对象

class Person
{
public:
	Person(int age)
	{
		this -> age = age; //此处左值需用this指针,否则无法表示成员变量age
	}

	int age;
};

这也是为什么大多数优秀程序员会选择将成员名加上前缀修饰,如age写成m_Age(m表示member),这样可以省去许多麻烦,也提高可读性。

返回对象本身用*this

考虑下面这段代码的输出

#include <iostream>
using namespace std;

class Person
{
public:
	Person(int age)
	{
		this->age = age;
	}
	
	void Addage(Person& p)
	{
		this->age += p.age;
	}
	
	int age;
};

int main(){
	Person p1(10);
	Person p2(10);
	
	p2.Addage(p1);
	cout << p2.age << endl;
	
	return 0;
}

显然,这段代码实现的是age的一个简单的叠加

那么我们可不可以实现累加呢?通过多次调用Addage()使得p2的age多次增加

或许可以这样做: p2.Addage(p1).Addage(p1).Addage(p1)

这样能否使得p2的age变成40呢?

实践出真知

报错了?因为Addage()返回的是一个空值,考虑原本的p2.Addage(),访问对象需要是一个对象,也就是说如果p2.Addage()的返回值仍是p2我们就能实现上述累加操作。

将Addage()函数略作修改

Person& Addage(Person& p)
	{
		this->age += p.age;
		return *this;
	}

这样我们就能实现多重套娃(bushi)

需要注意的是,定义的Addage返回值是Person&类型,是对被调用成员函数所属的对象的引用,如果用Person的话就是创建了一个新的对象,这与我们的原想法不同

结果如何读者可自行尝试

2.10空指针访问成员函数

考虑以下代码

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	void Say_hi()
	{
		cout << "Hi," << name << endl;
	}
	
	string name = "David";
};

int main(){
	Person *p = NULL;
	p->Say_hi();
	
	return 0;
}

该程序的输出结果为 Hi,   (部分编译器会直接报错)

这是因为在Say_hi()中调用了name,这实际上是this->name,由于p是空指针而非指向某个对象,所以运行结果不会是  Hi,David

我们可以修改Say_hi()函数,以避免后期因为传入空指针导致程序出错

void Say_hi()
	{
		if(this == NULL)
		{
			return;
		}
		cout << "Hi," << name << endl;
	}

2.11 const修饰成员函数

在类与对象中有常函数与常对象

常函数:

  • 成员函数后加const后我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时可加关键字mutable,在常函数中依然可以修改

考虑下面这段代码

#include <iostream>
using namespace std;

class Person
{
public:
	void show() const
	{
		this->m_A = 1; 
		this->m_B = 2;
		cout << m_A << ' ' << m_B << endl;
	} 
	
	int m_A = 0;
	mutable int m_B;
};

int main(){
	Person p1;
	p1.show(); 
	return 0;
}

测试后可以发现,编译器报错了,原因是在常函数show()中尝试修改成员变量m_A

在将这行语句(this->m_A = 1)注释掉后我们可以得到结果为 0 2

所以在成员函数后加const,本质上是修饰this指针,使其指向的值无法改变,而mutable则是创造一个特例

另外需要注意的是,this指针的本质 指针常量 其指向不可修改

也就是说下面这段代码会报错

#include <iostream>
using namespace std;

class Person
{
public:
	void show() const
	{
		this = NULL;  //this不能作为可修改的左值
	} 
};

int main(){

	return 0;
}

常对象:

  • 声明对象前加const称该对象为常对象
  • 常对象只能调用常函数

因为常对象中的成员属性不可修改,所以只能调用常函数,以确保不会修改成员属性

3.友元

3.1一点概述

友元就是声明一些特殊的函数或类,使其可以访问某类的私有成员

友元的关键字为   friend

友元的三种实现:

  • 全局函数作友元
  • 类作友元
  • 成员函数作友元

3.2三种实现

全局函数作友元

在类中声明该函数,并在前面加上friend即可

如:friend void fun_c(int n);     friend void goodGay(Building *building);

类作友元

与全局函数作友元类似,在类中friend并声明

如:friend class Building;

成员函数作友元

类似,只是语法不同

如:friend void GoodGay::visit();

4.运算符重载

概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

4.1加号运算符重载

可以通过两种方式实现加号运算符重载

  • 成员函数实现
  • 全局函数实现

多说无益,上代码,看实例

#include <iostream>
using namespace std;
class Person {
public:
	Person() { };
	Person(int a, int b)
	{
		this->m_A = a;
		this->m_B = b;
	}
	//成员函数实现 + 号运算符 
	Person operator+(const Person& p) {
		Person temp;
		temp.m_A = this->m_A + p.m_A;
		temp.m_B = this->m_B + p.m_B;
		return temp;
	}

public:
	int m_A;
	int m_B;
};

//全局函数实现 + 号运算符重载
//Person operator+(const Person & p1, const Person & p2) {
//	Person temp(0, 0);
//	temp.m_A = p1.m_A + p2.m_A;
//	temp.m_B = p1.m_B + p2.m_B;
//	return temp;
//}

//运算符重载 可以发生函数重载
Person operator+(const Person& p2, int val)
{
	Person temp;
	temp.m_A = p2.m_A + val;
	temp.m_B = p2.m_B + val;
}

void test() {
	Person p1(10, 10);
	Person p2(20, 20);

	//成员函数方式
	Person p3 = p2 + p1; //相当于 p2.operator+(p1)
	cout << "mA:" << p3.m_A << "mB:" << p3.m_B << endl;

	Person p4 = p3 + 10; //相当于 operator+(p3,10)
	cout << "mA:" << p4.m_A << "mB:" << p4.m_B << endl;
}

int main()
{
	test();
	return 0;
}

//本程序源自 黑马程序员
//找不到源码,跟着视频教学敲的

运算符重载后并没有彻底改变加号的运算法则

也就是说 cout << 1 + 2  的结果依然是 3

4.2左移运算符重载

在使用cout输出某些内容时用到的 << 就是左移运算符

重载左移运算符时只能使用全局函数,因为用成员函数无法实现cout在左侧

先上代码看实例

#include <iostream>
using namespace std;
class anupis
{
	friend void test(anupis p);
	friend ostream& operator<<(ostream& out, anupis& p);

public:
	anupis(int a, int b)
	{
		m_A = a;
		m_B = b;
	}

private:
	int m_A;
	int m_B;
};

ostream& operator<<(ostream& out, anupis& p)  //全局函数重构 out是形参,本质也是cout
{
	cout << "a:" << p.m_A << "b:" << p.m_B;
	return out;  //要使得后面可以继续输出,所以得返回out
}

void test(anupis p)
{
	cout << p << "Hello,World!" << endl; 
}

int main()
{
	anupis p(10,10);
	test(p);
	return 0;
}

左移运算符重载与加号运算符重载类似,主要是弄明白cout的数据类型

在Visual Studio中,我们可以通过右键cout转到定义来查看cout的定义

很明显,cout是ostream也就是输出流中的一个对象

由于iostream的拷贝构造函数不允许使用,所以我们只能传递地址而非值

这就是为什么ostream后要用&

ostream& operator<<(ostream& out, anupis& p) 
{
	cout << "a:" << p.m_A << "b:" << p.m_B;
	return out;  
}

4.3递增运算符重载

前置++运算符重载很简单

//重载前置++运算符  返回引用为了一直对一个数据进行递增操作
MyInteger& operator++()
{
	//先进行++运算
	m_A++;

	//再将自身做返回
	return *this;
}

重载后置++运算符:

MyInteger operator++(int)
{
	//先记录
	MyInteger temp = *this;
	//后递增
	m_A++;
	//最后返回
	return temp;
}

本来正常思路是先返回值,然后再递增,但返回值之后函数结束了就无法递增,所以得先记录值,在递增后才返回原本的值

要注意在此不需要使用&返回引用,而是返回值即可

因为我们使用的是局部的对象temp,如果返回的是引用,那么函数执行完后该对象被释放,我们获得的引用不存在,这是一个非法操作

所以重载前置++返回引用,重载后置++返回值,这是一个区别

4.4赋值运算符重载

类在创建之后本身会存在一个operator=的重载函数,也就是拷贝构造函数

但这样的拷贝构造函数隐含安全问题

假设有下面这样的代码段

#include <iostream>
using namespace std;
class Person
{
public:
	Person(int age)
	{
		m_Age = new int(age);
	}
	/*~Person()
	{
		delete m_Age;
	}*/

	int *m_Age;
};

int main()
{
	Person p1(18);
	Person p2(20);
	p2 = p1;
	cout << "p1:" << *p1.m_Age << ' ' << "p2:" << * p2.m_Age;
	return 0;
}

这段程序可以正常运行并输出 p1:18 p2:18

然而我们使用了堆区的内存,在使用后需要手动释放

在加上析构函数后会发现程序报错了

原因是 p2=p1 这样的赋值操作仅仅是将p1中m_Age的值赋给p2的m_Age

也就是说二者指向的是同一块内存,所以后面重复释放导致出错,这也是浅拷贝的一种

具体实现如下:

Person& operator=(Person& p)
{
	if (m_Age != NULL)
	{
		delete m_Age;
		m_Age = NULL;
	}

	m_Age = new int(*p.m_Age);  //重新分配内存
	return *this;  // *this也可以换成 p
}

4.5函数调用运算符重载

函数调用运算符 ()重载后也称为 仿函数

仿函数非常灵活,没有固定的写法

#include <iostream>
using namespace std;
class MyPrint
{
public:
	void operator()(string text)
	{
		cout << text << endl;
	}
};
void test1()
{
	//重载的()操作符 也称为仿函数
	MyPrint myFunc;
	myFunc("hello world");
}

class MyAdd
{
public:
	int operator()(int v1, int v2)
	{
		return v1 + v2;
	}
};

void test2()
{
	MyAdd add;
	int ret = add(10, 10);
	cout << "ret = " << ret << endl;

	//匿名对象调用
	cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

int main()
{
	test1();
	test2();

	return 0;
}

以上笔记想法来自B站黑马程序员教学视频

做作业发现的东西

string头文件中是c++独有的,包含许多内置函数

cstring则是C的东西,包括strcpy()这样方便的函数,但这样的函数在string中却是没有的

string中的函数主要是对两个以上字符串进行处理,较cstring更便捷,如:拼接,插入,删除,提取,查找

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值