类和对象(下)

目录

一.取地址运算符重载

1.1.const成员函数

1.2.取地址运算符重载

二.再探构造函数

三.类型转换

四.static成员

五.友元

5.1.代码一.

5.2.代码二.

六.内部类

​编辑

七.匿名对象

八.对象拷贝时的编译器优化


一.取地址运算符重载

1.1.const成员函数

  1. 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到 成员函数参数列表的后
    ⾯。
  2. const实际修饰该 成员函数隐含的this指针 ,表明在该成员函数中 不能对类的任何成员进⾏修改。
    const 修饰Date类的Print成员函数,Print隐含的this指针由 Date* const this 变为 const Date* const this,const修饰指针的"左定值,右定向"。
    下面我们通过代码实现:
cs 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void prin() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2025, 11, 1);
	d1.prin();
	Date d2(2025, 12, 1);
	d2.prin();
}

可以看到,当我们加上const,就确保了this指针指向的值不会发生改变。

1.2.取地址运算符重载

取地址运算符重载分为 普通取地址运算符重载 和 const取地址运算符重载 ,⼀般这两个函数编译器⾃动⽣成的就可以够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。

cs 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void prin() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	Date* operator&()
	{
		return this;//返回可修改的对象。
	}
	const Date* operator&()const
	{
		return this;//返回不可修改的对象。
	}
	//这两个都可以随便返回一个地址,让人产生误导
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 12, 1);
	const Date* p1= &d1;
	Date* p2 = &d1;
	cout << p1 << endl;
	cout << p2;
}

如果胡乱返回一个地址就会这样:

cs 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void prin() const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	Date* operator&()
	{
		return (Date*)0X11223344;
	}
	const Date* operator&()const
	{
		return (Date*)0X11223344;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 12, 1);
	const Date* p1 = &d1;
	Date* p2 = &d1;
	cout << p1 << endl;
	cout << p2;
}

就会这样产生误导

二.再探构造函数

  1. 之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅
    式,就是 初始化列表 ,初始化列表的使⽤⽅式是 以⼀个冒号开始 ,接着是⼀个 以逗号分隔 的数据成
    员列表, 每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。
  2. 每个成员变量在初始化列表中 只能出现⼀次 ,语法理解上初始化列表可以认为是 每个成员变量定义初始化的地⽅。
  3. 引⽤成员变量,const成员变量,没有默认构造的类类型变量 ,必须 放在初始化列表 位置进⾏初始化,否则会编译报错。
  4. C++11⽀持 在成员变量声明的位置给缺省值 ,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。
  5. 尽量使⽤初始化列表初始化,因为那些你 不在初始化列表初始化的成员也会⾛初始化列表 ,如果这个成员 在声明位置给了缺省值 , 初始化列表会⽤这个缺省值初始化 。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化 取决于编译器 ,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  6. 初始化列表中 按照成员变量在类中声明顺序进⾏初始化 ,跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
    我们来总结一下:
    每个构造函数都有初始化列表,每个成员都要走初始化列表
  7. 在初始化列表初始化的成员(显示写)
  8. 没有在初始化列表初始化的成员(不显示写)
    a.声明的地方有缺省值用缺省值
    b.没有缺省值:
    x:内置类型不确定,大概率是随机值
    y:自定义类型,调用默认构造函数,没有默认构造函数就编译报错
  9. 引用 const没有默认构造自定义,必须在初始化列表初始化
    下面我们来看一段代码(这段代码主要是让我们巩固第6点的知识):
cs 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1) {
	}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa(1);
	aa.Print();
}

运行结果:

为什么会是这样的结果呢,我们可以看到,成员变量在类中声明时,最先声明的是a2,所以a2应该是最先初始化,但是初始化a2的是a1,这时候a1还没有被初始化,是一个随机值,所以a2也是一个随机值。a2初始化之后,a1才被1初始化。

三.类型转换

  1. C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  2. 构造函数前⾯加explicit就不再⽀持隐式类型转换。
  3. 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。
cs 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	//explicit A(int a = 0)
	A(int a = 0)
	{
		_a1 = a;
	}

	A(const A& aa)
	{
		_a1 = aa._a1;
	}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
	}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};

class Stack
	{
	public:
		void Push(const A& aa)
		{
			//...
		}
	private:
		A _arr[10];
		int _top;
	};

int main()
{
	A aa1(1);
	aa1.Print();
	A aa2 = 2;
	aa2.Print();

	A& raa1 = aa2;
	const A& raa2 = 2;

	int i = 1;
	double d = i;
	const double& rd = i;

	Stack st;

	A aa3(3);
	st.Push(aa3);

	st.Push(3);

	// C++11
	A aa5 = { 1, 1 };
	const A& raa6 = { 2,2 };
	st.Push(aa5);
	st.Push({ 2,2 });

	return 0;
}

A aa2 = 2;

C++ 标准允许将[隐式转换生成临时对象 + 拷贝构造]优化为 直接调用构造函数(即A aa2=2),省去拷贝构造的开销;
1. int 2 隐式转换为 A 类型的临时对象 (调用 A(int) 构造,临时对象地址假设为 0x123);

  1. 用这个临时对象拷贝构造 aa2(调用 A(const A&));

  2. 拷贝构造完成后,临时对象被销毁(生命周期结束)。

const A& raa2 = 2;

1. 2隐式转换为 A 临时对象(生命周期本应很短,构造后立即销毁);

  1. const A& 绑定后,C++ 会延长临时对象的生命周期 (和引用 raa2 同生命周期)临时对象具有常性;(引用临时对象)

  2. 若允许非 const 引用绑定(A& raa2 = 2),意味着你可以通过引用修改临时对象(如 raa2._a1 = 10),但临时对象很快会销毁,修改毫无意义,还可能引发内存问题 ------ 因此编译器直接禁止这种写法,强制要求用 const 限制 "只读访问"。

再c++11之前只能用单参数进行类型转换,但c++11之后多个参数就可以通过花括号包含传入。

四.static成员

  1. ⽤static修饰的成员变量,称之为 静态成员变量 ,静态成员变量⼀定要 在类外进⾏初始化 。
  2. 静态成员变量为 所有类对象所共享 ,不属于某个具体的对象,不存在对象中, 存放在静态区 。
  3. ⽤static修饰的成员函数,称之为静态成员函数, 静态成员函数没有this指针 。
  4. 静态成员函数中 可以访问其他的静态成员 ,但是 不能访问⾮静态的 ,因为没有this指针。
  5. ⾮静态的成员函数,可以访问任意的 静态成员变量 和 静态成员函数 。
  6. 突破类域就可以访问静态成员,可以通过 类名::静态成员 或者 对象.静态成员 来访问 静态成员变量 和 静态成员函数 。
  7. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
  8. 静态成员变量 不能在声明位置给缺省值初始化 ,因为 缺省值是个构造函数初始化列表 的, 静态成员变量不属于某个对象 ,不⾛构造函数初始化列表。
    下面我们来写一个代码深入了解:
cs 复制代码
#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		_sourt++;
	}
	A(const A& a)
	{
		_sourt++;
	}
	~A()
	{
		--_sourt;
	}
	static int getsourt()
	{
		return _sourt;
	}
private:
	static int _sourt;
};

int A::_sourt = 0;

int main()
{
	cout << A::getsourt() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::getsourt() << endl;
	cout << a1.getsourt() << endl;
	// 编译报错:error C2248: "A::_scount": ⽆法访问 private 成员(在"A"类中声明)
	//cout << A::_scount << endl;
	return 0;
}

静态成员函数属于类本身,不依赖任何对象实例,无需创建对象,直接通过类名::函数名调用,当然通过创建对象调用也是可以的,非静态成员函数属于对象实例,必须依赖具体的对象,必须用对象.函数名进行调用。

下面我们来做一道练习题,更好的了解静态成员变量和成员函数的好处:

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。数据范围: 0<n≤2000,进阶: 空间复杂度 O(1) ,时间复杂度 O(n)

cs 复制代码
#include<iostream>
using namespace std;
class sum {
  public:
    sum() {
        i += 1;
        all += i;
    }
    static int getall() {
        return all;
    }
  private:
    static int all;
    static int i;
};
int sum:: i = 0;
int sum::all = 0;
class Solution {
  public:
    int Sum_Solution(int n) {
        sum a[n];
        return sum::getall();
    }
};
int main() {
    int k = 0;
    cin >> k;
    Solution so;
    cout << so.Sum_Solution(k);
    return 0;
}

可以看到这个题目将我们所有的路基本上都堵死了,在我们学习C语言的时候,我们通常用for循环来做这道题,但是既然有这么多的限制,我们就只能拿出static成员变量了,sum a[n];,这个数组中的每一个元素都是一个独立的sum类对象,创建时会自动调用sum的默认构造函数,使得static修饰的成员变量按照代码增加。最终得到想要的结果。

五.友元

  1. 友元提供了⼀种突破类访问限定符封装的⽅式,友元分为: 友元函数 和 友元类 ,在函数声明或者类声明的前⾯加friend,并且 把友元声明放到⼀个类的⾥⾯ 。
  2. 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明, 他不是类的成员函数 。
  3. 友元函数可以在 类定义的任何地⽅声明 ,不受类访问限定符限制。
  4. ⼀个函数可以是多个类的友元函数。( 一对多 )
  5. 友元类 中的 成员函数 都可以 是另⼀个类的友元函数 ,都可以访问另⼀个类中的私有和保护成员。
  6. 友元类的关系是 单向的 , 不具有交换性 ,⽐如A类是B类的友元,但是B类不是A类的友元。
  7. 友元类 关系不能传递 ,如果A是B的友元, B是C的友元,但是A不是C的友元。
  8. 有时提供了便利。但是友元 会增加耦合度 , 破坏了封装 ,所以友元 不宜多用 。
    下面我们写几段代码进行深入了解:

5.1.代码一.

一个函数可以是多个类的友元函数:

cs 复制代码
#include<iostream>
using namespace std;
class B;
class A
{
public:
	friend int fun(A& a, B& b);
private:
	int _a1=1;
	int _a2=2;
};

class B
{
public:
	friend int fun(A& a, B& b);
private:
	int _b1 = 1;
	int _b2 = 2;
};

int fun(A& a, B& b)
{
	return a._a1 + b._b1;
}

int main()
{
	A a;
	B b;
	cout<<fun(a, b);
}

5.2.代码二.

友元类:

cs 复制代码
#include<iostream>
using namespace std;
class A
{
	friend class B;
	int _a1 = 1;
	int _a2 = 0;
};

class B
{
public:
	void prin()
	{
		A a;
		cout << a._a1 << endl;
		cout << _b1 << endl;
	}
private:
	int _b1 = 0;
	int _b2 = 3;
};

int main()
{
	B b;
	b.prin();
	return 0;
}

friend class B;友元仅授予「访问权限」,不会自动创建对方类的对象 ------B 要访问 A_a1,必须通过 A 的实例(对象),不能直接写 cout << _a1 << endl;(编译器不知道 _a1 属于哪个 A 对象)。

六.内部类

  1. 如果⼀个类定义在另⼀个类的内部,这个定义在内部的类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

  2. 内部类默认是外部类的友元类。

  3. 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其 他地⽅都⽤不了。

cs 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	class B
	{
	public:
		int _b1;
		void foo(A& a)
		{
			cout << a._a2 << endl;
			cout << k << endl;
		}
	};
private:
	static int k;
	int _a2 = 4;
};
int A::k = 0;
int main()
{
	cout << sizeof(A) << endl;
	A::B b;
	A aa;
	b.foo(aa);
	return 0;
}

在这段代码中,我们定义了一个在类A中的内部类B,通过sizeof计算类A的字节数,可以验证,定义在类A中的内部类B,以及静态成员变量k都不属于类A,它们只是受到了类A这个类域的限制。

下面我们回到前面的题目,看看用内部类是否能够解决前面的题目:

cs 复制代码
#include<iostream>
using namespace std;
class Solution {
    class sum {
    public:
        sum() {
            i += 1;
            all += i;
        }
        static int getall() {
            return all;
        }
    };
    static int all;
    static int i;
public:
    int Sum_Solution(int n) {
        sum a[n];
        return sum::getall();
    }
};
int Solution::i = 0;
int Solution::all = 0;
int main() {
    int k = 0;
    cin >> k;
    Solution so;
    cout << so.Sum_Solution(k);
    return 0;
}

七.匿名对象

  1. 用 类型(实参) 定义出来的对象叫做匿名对象 ,相⽐之前我们定义的 类型对象名(实参)定义出来的叫有名对象
  2. 匿名对象 ⽣命周期只在当前⼀行 ,⼀般临时定义⼀个对象 当前⽤⼀下即可 ,就可以 定义匿名对象。
cs 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 1) :_a1(a)
	{
		i++;
		cout <<"A(int a)" << endl;
	}
	~A()
	{
		cout << i << "~A()" << endl;
	}
private:
	static int i;
	int _a1;
};
int A::i = 0;

int main()
{   
    A(1);
	A a2;
	A a3(2);
	return 0;
}

八.对象拷贝时的编译器优化

  1. 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下 会尽可能减少⼀些传参和传返回值的过程中可以省略的拷⻉。
  2. 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的 编译器对于连续⼀个表达式步骤中的连续拷⻉会进⾏合并优化 ,有些更新更"激进"的编译器还会进⾏跨⾏跨表达式的合并优化。
  3. linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的⽅式关闭构造相关的优化。

相关推荐
小年糕是糕手1 小时前
【C++同步练习】类和对象(一)
java·开发语言·javascript·数据结构·c++·算法·排序算法
运维小文1 小时前
Centos7部署.net8和升级libstdc++
开发语言·c++·.net
小年糕是糕手1 小时前
【C++同步练习】类和对象(二)
java·开发语言·javascript·数据结构·c++·算法·ecmascript
xixixi777771 小时前
解析常见的通信流量和流量分析
运维·开发语言·网络·安全·php·通信·流量
csdn_aspnet1 小时前
用Python抓取ZLibrary元数据
开发语言·python·zlibrary
天下无敌笨笨熊1 小时前
kotlin常用语法点理解
android·开发语言·kotlin
hazhanglvfang2 小时前
使用curl测试java后端post接口
java·开发语言
杀死那个蝈坦2 小时前
Lua核心认知
开发语言·lua
量子炒饭大师2 小时前
David自习刷题室——【蓝桥杯刷题备战】乘法表
c语言·c++·git·职场和发展·蓝桥杯·github·visual studio