您现在的位置是:首页 >技术交流 >【C++从0到王者】第六站:类和对象(下)网站首页技术交流

【C++从0到王者】第六站:类和对象(下)

青色_忘川 2024-06-17 10:43:23
简介【C++从0到王者】第六站:类和对象(下)

一、再谈构造函数

1.构造函数体赋值

如下代码所示,当我们写构造函数的时候,我们通常会在函数体内进行赋值。这就是构造函数体赋值

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

2.初始化列表

1>初始化列表的使用

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。初始化列表是构造函数的一部分
如下所示是初始化列表的基本使用

class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

2>初始化列表的注意事项

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    引用成员变量
    const成员变量
    自定义类型成员(且该类没有默认构造函数时)

这三种情况必须使用初始化列表的原因就是因为他们初始化后就不能被修改了,而构造函数体赋值并不是初始化,是一个赋值操作。初始化列表其实就是对象的成员定义的位置
如下代码所示

class A
{
public:
	A(int a,int b)
	{
		cout << "A" << endl;
	}
private:
	int a;
};
class B
{
public:
	B(int i, int& ref)
		:_ref(ref)
		, i(i)
		,aa(1,2)
	{
		cout << "B" << endl;
	}
private:
	int& _ref;
	const int i;
	A aa;
};
int main()
{
	int n=0;
	B b(1, n);
	return 0;
}

事实上,我们的构造函数中,即便不写,也有初始化列表。只不过对于内置类型而言这个初始化列表什么也不做,对于自定义类型,他的初始化列表就相当于调用它对于的默认构造函数,而一旦该自定义类型没有默认构造函数的时候,那么就必须要有初始化列表了。
在这里插入图片描述我们有时候也会给成员变量缺省值,这个缺省值的作用其实就是给初始化列表,当我们没有显示的在初始化列表上赋值的时候,那么缺省值就会给初始化列表

有了初始化列表,我们就可以这样做,在之前的栈实现队列中,我们就可以显示的指定栈的容量进行初始化

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}

		_size = 0;
		_capacity = capacity;
	}

	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};

class MyQueue
{
public:
	MyQueue()
	{}

	MyQueue(int capacity)
		:_pushst(capacity)
		,_popst(capacity)
	{}

private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	MyQueue q1;
	MyQueue q2(100);

	return 0;
}
  1. 初始化列表是构造函数的一部分,它与函数体赋值是相辅相成的,并不能相互替代
    这是我们需要注意的一点,因为有些时候在函数体中有其他事情要做,比如检查内存申请是否成功等

如下所示,是需要检查是否申请内存成功

class Stack
{
public:
	Stack(int capacity = 4)
		:_arr((int*)malloc(sizeof(int)* capacity))
		, _top(0)
		, _capacity(capacity)
	{
		if (_arr == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memset(_arr, 0, sizeof(int) * _capacity);
	}
private:
	int* _arr;
	int _top;
	int _capacity;
};

如下所示是为了动态开辟一个二维数组

class AA
{
public:
	AA(int row = 10, int col = 5)
		:_row(row)
		,_col(col)
	{
		_aa = (int**)malloc(sizeof(int*) * _row);
		int i = 0;
		for (i = 0; i < _col; i++)
		{
			_aa[i] = (int*)malloc(sizeof(int) * _col);
			memset(_aa[i], 0, sizeof(int) * _col);
		}
	}
private:
	int** _aa;
	int _row;
	int _col;
};
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

我们可以看这段代码的运行结果

class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}

在这里插入图片描述这是因为初始化列表是按照成员变量的顺序走的,也就是先初始化_a2,由于_a1并不知道,所以就是被初始化为了随机值。然后才将_a1初始化为1

3.explicit关键词

我们先来看这段代码

class A
{
public:
	A(int a)
		:_a2(a)
		,_a1(a)
	{}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa1(1);
	A aa2 = 2;

	return 0;
}

这段代码我们可能会疑惑:A aa2=2这个语句。这里其实是隐式的类型转换。整型转化为了自定义类型
我们在之前说过,在发生类型转换时候,会产生一个临时变量,如下代码所示,将i类型转换为double类型,然后拷贝给d

int i=1;
double d=i;

所以前面的代码也是同理的:2先构造一个A的临时对象,在将这个临时变量拷贝构造给A
但是现在的编译器都会在这里进行一次优化,优化用2直接构造给A,只需要构造一次,跳过了拷贝构造环节
在这里插入图片描述我们可以从下面的代码验证拷贝构造的存在
在这里插入图片描述在这里插入图片描述第一个报错的原因是,临时变量具有常性,1构造出来的A临时对象,使用aa3作为别名的时候,类型是A&,权限放大。所以报错
第二个正确是因为,aa3此时是const A&类型,可以直接引用这个临时变量。权限平移

当然我们有时候也期望说能不能不要发生这些隐式类型转换,我们就可以加上explicit关键词,这个关键词加在构造函数的前面在这里插入图片描述

二、static成员

1.如何统计当前程序中变量的个数

我们最容易想到的方法就是创建一个全局变量,当调用构造和拷贝构造的时候,全局变量++,当调用析构函数的时候,全局变量–

int _scount = 0;
class A
{
public:
	A(){_scount++;}
	A(const A& a) { _scount++;}
	~A() { _scount--; }
private:
};
A a0;
A Func(A a)
{
	cout << __LINE__ << ":" << _scount << endl;
	A a4;
	cout << __LINE__ << ":" << _scount << endl;
	return a4;
}
int main()
{
	cout << __LINE__ << ":" << _scount << endl;
	A a1;
	static A a2;
	Func(a1);
	cout << __LINE__ << ":" << _scount << endl;

	return 0;
}

在这里插入图片描述还有如下的程序,下面的程序是在函数中使用了静态的变量,这样的话它这个变量就放在了静态区,只会被创建一次

int _scount = 0;
class A
{
public:
	A() { _scount++; }
	A(const A& a) { _scount++; }
	~A() { _scount--; }
private:
};
A a0;
void  Func()
{
	static A a4;
	cout << __LINE__ << ":" << _scount << endl;
}
int main()
{
	cout << __LINE__ << ":" << _scount << endl;
	A a1;
	static A a2;
	Func();
	Func();

	return 0;
}

在这里插入图片描述

但是上面两个代码都有一个劣势,那就是全局变量极其容易被修改。导致出现问题。为了避免这个问题,我们可以采用静态的方法,将这个变量放在成员变量用static修饰。也就是静态成员变量

成员变量和静态成员变量还是存在区别的。成员变量属于每一个类对象。静态成员变量不是属于某个对象,而是属于类的,属于每个类的对象共享,存储在静态区。生命周期是全局的。

  1. 由于静态成员变量并不属于某个类对象,所以它不可以在初始化列表进行初始化,更不可能使用缺省值。
  2. 他在类中仅仅只是一个声明,定义它的初始值需要在全局位置:类外进行定义。在类外进行定义的时候,该静态成员变量可以是私有的,可以是公有的,这里可以突破一次。
  3. 但是定义之后,再次使用这个静态成员变量。可以是将其设置为公有的,然后直接使用域作用限定符进行调用。或者利用一个对象调用它。
  4. 但是大多数时候是私有的,我们可以通过一个公有的成员函数进行获取它的值。这里又牵扯到一个问题:如果调用公有的成员函数?如果有对象,那自然可以借助对象来调用成员函数。如果没有对象呢?我们就需要使用静态成员函数了。静态成员函数是没有this指针的,只需要类域和访问限定符就可以访问这个函数了
  5. 还需要注意的是,静态成员函数里面是不可以访问到非静态的成员变量。因为它没有this指针
class A
{
public:
	A() { _scount++; }
	A(const A& a) { _scount++; }
	~A() { _scount--; }
	static int GetScount()
	{
		return _scount;
	}
private:
	//成员变量
	int a;
	int b;
//public:
	//静态成员变量
	static int _scount;
};
int A::_scount = 0;
A a0;
void  Func()
{
	static A a4;
	cout << __LINE__ << ":" << A::GetScount() << endl;
}
int main()
{
	cout << __LINE__ << ":" << A::GetScount() << endl;
	A a1;
	static A a2;
	Func();
	Func();
	cout << __LINE__ << ":" << a2.GetScount() << endl;

	return 0;
}

2.static的特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
  6. 类成员函数可以访问静态成员函数,但是静态成员函数不可调用非静态的,因为非静态需要this指针,而静态没有this指针

3.从1加到n

我们来看这样一道题:从1加到n
这道题有很多的限制。我们假定在限制掉递归和位操作符。那么如何解决这道题呢?其实这就和前面的计算有多少个对象有异曲同工之妙。
如下代码所示,是我们给出的解法:我们在定义一个类,这个类有两个静态成员变量。我们先将其在类外初始化为0,当每调用一次构造函数的时候,count++,然后让ret加上count。最后我们直接在原来的类里面创建n个对象即可。就能得出最终结果,创建n个对象我们可以使用数组或者new来实现,但是不可以使用malloc。因为malloc不调用构造函数

class A
{
public:
    A()
    {
        count++;
        ret+=count;
    }
    static int Getret()
    {
        return ret;
    }
private:
    static int count;
    static int ret;
};
int A::count=0;
int A::ret=0;
class Solution {
public:
    int Sum_Solution(int n) {
        A a[n];
        return A::Getret();
    }
};

4.设计一个类,只能在栈或者堆上开辟空间

这个要求听起来是比较奇怪的,要求我们只能在栈区或者堆区开辟空间的话,就得先想办法控制住其他的开辟不了空间。我们可以直接将构造函数放到私有区域中,直接让开辟不了空间,然后我们在创建两个静态成员函数,这两个静态成员函数可以在栈区和堆区开辟空间。然后我们直接返回即可。这同样是应用了静态成员函数的特点

class A
{
public:
	static A GetStack()
	{
		A a;
		return a;
	}
	static A* GetHeap()
	{
		return new A;
	}
private:
	A()
	{};
};
int main()
{
	A a = A::GetStack();
	A* b = A::GetHeap();
	return 0;
}

三、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类

1.友元函数

我们在前面实现日期类的时候,对于流插入和流提取运算符的重载,我们不能在类内进行定义,因为this指针默认占据了第一个形参。为了交换参数的顺序,我们必须得将这两个运算符重载写到全局中。但是这样的话我们又出现了一个问题,我们无法访问成员变量。所以我们就使用了友元函数声明,你是我的朋友,所以可以来使用我的成员变量
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数
  2. 友元函数不能用const修饰,因为没有this指针
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

2.友元类

** 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。**

  1. 友元关系是单向的,不具有交换性。
  2. 比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
  3. 类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  4. 友元关系不能传递
  5. 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  6. 友元关系不能继承
class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

四、内部类

1.内部类的概念

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

2.内部类的特性

特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。内部类都可以访问外部类的成员。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。访问外部类的非静态成员就需要使用对象了。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
  4. 如果想要在类外定义内部类的对象,那么就需要通过作用限定符来找到内部类,在定义对象。如果内部类是私有的,那么就无法定义内部类的对象
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	cout << sizeof(A) << endl;
	A a;
	A::B b;
	b.foo(A());
	return 0;
}

3.从1加到n

我们继续看前面的这道题,我们可以借助内部类的特性,将原来的A类定义在Solution类里面,将A类的静态成员给Solution类。这样我们就利用内部类可以访问外部类的成员变量特性。从而实现我们的题目

class Solution {
    class A
    {
    public:
        A()
        {
            count++;
            ret += count;
        }
    };
public:
    int Sum_Solution(int n) {
        A a[n];
        return ret;
    }
private:
    static int count;
    static int ret;
};
int Solution::count=0;
int Solution::ret=0;

五、匿名对象

所谓匿名对象,就是没有名字的对象,它的声明周期只有那一行,即用即销毁

class A
{
public:
	A(int a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
	int ADD(int x ,int y)
	{
		cout << "int ADD(int x ,int y)" << endl;
		return x + y;
	}
};
int main()
{
	A a(10);//有名对象
	A(10);//匿名对象

	a.ADD(1, 2);
	A(10).ADD(1, 2);
}

在这里插入图片描述还需要注意的是:匿名对象具有常性
在这里插入图片描述并且加上const以后声明周期被延长了,声明周期延长为当前函数的局部域
在这里插入图片描述

六、拷贝构造中编译器的一些优化

如下类所示,我们将对下面的这个类做出一些操作

class A
{
public:
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a)" << endl;
    }
    A(const A& aa)
        :_a(aa._a)
    {
        cout << "A(const A& aa)" << endl;
    }
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa)" << endl;

        if (this != &aa)
        {
            _a = aa._a;
        }

        return *this;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
private:
    int _a;
};

我们知道如下几个比较简单的情况

  1. 传值调用需要调用拷贝构造函数,而传引用调用不需要拷贝构造函数
    在这里插入图片描述
  2. 当我们在函数内定义了A类型对象以后,返回这个对象的时候,是需要拷贝构造出一个临时对象的。
    在这里插入图片描述
  3. 当我们传引用返回的时候是没有拷贝构造的
    在这里插入图片描述

上面是一些比较简单的场景,下面就是一些比较复杂的场景了

  1. 像下面这种写法是错误的,因为返回的是一个临时变量。临时变量具有常性。涉及权限放大

在这里插入图片描述为了处理这个权限放大,我们可以在a的前面使用const进行修饰

在这里插入图片描述

  1. 同一行一个表达式中连续的构造+拷贝构造优化合二为一

下面这种写法是几次拷贝?

A Func5()
{
    A a;
    return a;
}
int main()
{
    A a= Func5();
}

我们会认为拷贝了两次,一次是返回的时候要生成临时变量,这是一次拷贝构造,在一次就是将临时变量拷贝构造出a。也就是两次。
但是现在的编译器会认为这个是比较浪费的,所以现在的编译器会处理这两次拷贝为一次拷贝
在这里插入图片描述在这里插入图片描述

  1. 连续的两个构造或两个拷贝构造或一个拷贝构造和一个构造会进行优化

在这里插入图片描述

如果我们分开写,先定义一个对象,在进行赋值的话,这样的代价大大提升了
前者合起来的只有一次拷贝构造。后者先进行构造,然后拷贝构造,然后赋值运算符重载。开销大大提升
在这里插入图片描述

七、再次理解类和对象

类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
在这里插入图片描述


好了,本期内容就到这里了
如果对你有帮助的话,不要忘记点赞加收藏哦!!!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。