类与对象(上)

发布于:2025-02-10 ⋅ 阅读:(56) ⋅ 点赞:(0)

目录

1.引入

2.类的定义

3.this指针

4.默认成员函数

4.1构造函数

4.2析构函数

4.3拷贝构造函数

运算符重载:

4.4赋值运算符重载


1.引入

C语言是面向过程的,但C++是面向对象的,这一点很好地体现在了类上面,什么是类?

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数,这时struct就上升到了类:

typedef int DataType;
struct Stack
{
	void Init(size_t capacity)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(const DataType& data)
	{
		// 扩容
		_array[_size] = data;
		++_size;
	}
	DataType Top()
	{
			return _array[_size - 1];
	}
	void Destroy()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
	DataType* _array;
	size_t _capacity;
	size_t _size;
};
int main()
{
	Stack s;
	s.Init(10);
	s.Push(1);
	s.Push(2);
	s.Push(3);
	cout << s.Top() << endl;
}

在这个用c++中的类模拟实现的栈中,在类中定义了函数,并且在类中对变量定义的顺序也没有特别规定,c++默认都在一个类中,是一个整体,并且可以在其中定义函数。

2.类的定义

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面 号不能省略
类体中内容称为类的成员:类中的变量称为类的属性成员变量; 类中的函数称为类的方法或者 成员函数
类的两种定义方式:
1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成 联函数处理。(建议不要在类中加上inline内联,因为类中默认函数内联,但具体是否内联取决于函数大小,是否递归以及编译器)。
2. 类中成员函数声明放在.h文件中,定义放在.cpp文件中,注意:成员函数名前需要加类名::,(否则在类中定义的某些变量无法正常使用)并且如果要用到缺省参数,注意在声明中提前定义!
类的访问限定符及封装:
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用
【访问限定符说明】
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

补充:C++中struct和class的区别是什么?

C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。

成员变量命名规则的建议:
class Date
{
public:
 void Init(int year)
 {
 _year = year;
 }
private:
 int _year;//这里加斜杠就是防止形参与这里的变量混淆,以示区分
};
类的作用域:
1.类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::
作用域操作符指明成员属于哪个类域。
2.在不同的类中可以定义同名函数,这也是因为存在类的作用域。
类的实例化:

1.用类类型创建对象的过程,称为类的实例化,类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它

2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
举个例子:
typedef int DataType;
class Stack
{
public:
	void Init(size_t capacity)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(const DataType& data)
	{
		// 扩容
		_array[_size] = data;
		++_size;
	}
	DataType* _array;
	size_t _capacity;
	size_t _size;
};
int main()
{
	Stack s;
	s._capacity = 1;
	return 0;
}

这里必须先要创建一个栈出来,不能直接对类中的元素赋值,可以理解为类只是一张图纸,什么都没有,只有根据这张图纸盖出房子来,才可以住人。

类对象模型:
如何计算类对象的大小?
1.类中函数是放在 公共代码段的,因此在计算类的大小时不需要计算函数所占用的空间,只需要计算成员变量的占用空间。
2. 一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐,(其原理与结构体内存对其规则是相同的,即是所占用的空间大小是最大对齐数的整数倍,其余的空间浪费掉,根据自定义类型中结构体内存对齐的具体介绍,本质上是空间换取时间的做法)
结构体内存对齐规则
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
3. 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象,因此当类中没有成员函数或者成员变量时,这是去计算这个类的大小,就是一个字节,这是编译器规定的。

3.this指针

先来看一段代码的结果:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

明显这其中的Init函数以及Print函数都是放在公共代码段的,那么为什么会打印出不一样的结果呢?为什么不会发生冲突呢?

这是因为this的作用,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

this 指针的特性:
1. this指针的类型:类类型* const,(这里this指针指向的地址被const修饰,因此不可以在成员函数中修改this指针的地址,但可以修改指向的内容),成员函数中, 不能明显传值this指针,但在类中可以引用this指针。
2. 只能在“成员函数”的内部使用。
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中 不存储this指针。 (this指针的实质上是形参,即在栈上开辟空间,存储地址)
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
eg:
// 1.下面程序编译运行结果是?
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

这里打印print()并没有对this解引用,因此即使this指针为空,运行也是正确的!        

// 1.下面程序编译运行结果是?
class A
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->PrintA();
	return 0;
}

这时候就不一样了,将this的空指针传值过去后,对this试图解引用访问_a,这当然是错误的,这时就会报错。

4.默认成员函数

任何类在什么都不写时,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

4.1构造函数

如何对一个类进行初始化呢?我们可以在类中写一个init函数,然后完成类的实例化之后,调用类中的初始化函数,在C++11中存在另一种初始化方式;

概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

特性:构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
typedef int DataType;
class Stack
{
public:
	Stack(DataType* a, int n)
	{
		cout << "Stack(DataType* a, int n)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * n);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		memcpy(_array, a, sizeof(DataType) * n);

		_capacity = n;
		_size = n;
	}

	Stack(int capacity = 4)
	{
		cout << "Stack(int capacity = 4)" << endl;
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

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

	void Pop()
	{
		if (Empty())
			return;
		_size--;
	}

	DataType Top() { return _array[_size - 1]; }
	int Empty() { return 0 == _size; }
	int Size() { return _size; }

	

private:
	void CheckCapacity()
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity * 2;
			DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
			if (temp == NULL)
			{
				perror("realloc申请空间失败!!!");
				return;
			}
			_array = temp;
			_capacity = newcapacity;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

上面这段代码演示了用类来作为构造函数名并且构造函数构成重载。

如果我们在main函数中这样去使用构造函数呢?

int main()
{
	Stack s(1);
	Stack ss();
	return 0;
}

那么第一种就是正确的,而第二种是错误的,原因是:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,编译器会报警告:

1>\test.cpp(370,8): warning C4930: “Stack ss(void)”: 未调用原型函数(是否是有意用变量定义的?)

6. 不实现构造函数的情况下,编译器会生成默认的构造函数。C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是int,char,double或者是指针,自定义类型就是我们使用的class/struct/union等自己定义的类型。
再来看例子:我们将编译器对内置类型自动调用默认构造函数产生的初始值打印出来:
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

结果却是随机值,这是为什么?我们不写构造函数,那么编译器会自动调用默认构造函数,通常对内置类型不做处理,这在不同的编译器上有着不同的效果,有的可能会主动赋值为0,这里演示的是在vs2022上的处理结果,而自定义类型会去调用它的默认构造。

难道只能让编译器产生随机值吗?C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

例如:

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
总结一下:
1.一般情况下,有内置类型成员的,就需要自己去写构造函数,而不能用编译器自己生成的默认构造函数,因为这样会产生随机值。
2.如果类中全部都是自定义类型成员,那么可以考虑让编译器去自动调用构造函数。

4.2析构函数

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
特性 :
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意: 析构
函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

5.与构造函数类似,当没有显式去写析构函数时,编译器会自动调用析构函数,通常对内置类型不做处理,而对自定义类型会调用析构函数。

总结一下:

1.一般情况下,有动态申请资源,就需要显式写析构函数释放空间。

2.没有动态申请的情况下,不需要些析构。

3.需要释放资源的成员类型是自定义类型时,不需要写析构。

4.3拷贝构造函数

拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存
在的类类型对象创建新对象时由编译器自动调用
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个 必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
eg:
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d) 
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

这里是先初始化d1,然后用d1来初始化d2,请注意,拷贝构造函数在本质上也是一种构造函数,构造函数是可以重载的,但这里不能设置为Date d,是因为拷贝构造函数的特性

1.内置类型会直接赋值拷贝;

2.自定义类型会调用拷贝构造函数;

如果这样,那么传参d1时又会调用一次拷贝构造函数,就这样无限循环递归下去,形成死循环。因此我们显式写的拷贝构造函数就要用引用类型接收。

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。但是这样存在一定的缺点,对于内置类型来说可以原封不动地初始化新的同类,但对于自定义类型来说,如果对于地址也是一摸一样的话,在析构时就会发生错误。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。这需要我们自己去实现深拷贝。在深拷贝中,为了防止将赋值对象颠倒,建议在参数前面加上const修饰,以示区分。

运算符重载:

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
  注意:
1. 不能通过连接其他符号来创建新的操作符:比如operator@
2. 重载操作符必须有一个类类型参数,如果全部都是内置类型
3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this.
5. .* :: sizeof ?: . 注意以上5个运算符不能重载。 
如果我们要判断两个日期是否相等,可以创建一个公有的运算符重载函数:
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
   && d1._month == d2._month
        && d1._day == d2._day;
}

但如果把运算符重载函数放进类中呢?就需要像这样:

 bool operator==(const Date& d2)
 {
        return _year == d2._year;
            && _month == d2._month
            && _day == d2._day;
 }

这是因为类中的成员函数,会传参this指针,且可以访问到private里的成员变量,所以类中的运算符重载函数有两个变量,一个是this指针,另一个是const修饰的引用类型变量,用const和引用的原因分别是防止传参错误和提高传参效率。(注意这里传参时并没有去调用默认拷贝函数,因为传参类型属于指针,而指针属于内置类型,直接传参)

4.4赋值运算符重载

1.赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
例如:
class Date
{
public:
	Date(int year = 2025, int month = 1, int day = 13)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;//这里返回*this并没有错,外部的Date还存在
	}
private:
	int _year;
	int _month;
	int _day;
};

这里实现的是三个Date类型的统一初始化。

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
这是因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(这里其实是浅拷贝)注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现。(自己完成深拷贝赋值运算符重载函数)否则在析构时就可能会出错!
补充:如果是已经存在的两个对象之间进行复制拷贝,则属于赋值运算符函数重载;
           如果是用一个已经存在的对象去初始化另一个对象,则属于拷贝构造。
 

网站公告

今日签到

点亮在社区的每一天
去签到