类和对象的基础认识

本文详细讲解了C++中类和对象的概念,类的定义、封装特性、成员函数与成员变量、构造函数、析构函数、拷贝构造与赋值重载,以及运算符重载。重点讨论了封装、静态成员和友元等内容,适合理解面向对象编程的初学者。
摘要由CSDN通过智能技术生成

类和对象

定义一个类就是定义一个新的类型

c语言中定义一个含有多种类型的新类型用结构体来定义

c语言面向过程定义一个类型认为,数据方法是分离的,数据就是数据,方法是方法。(函数也叫方法)

c++面向过程,数据和方法封装在一起

  • struct在c++中也可以定义类,但是在c++中更喜欢用class关键字来定义类

类的定义

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。 类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

{}就是一个域

访问限定符

public修饰的成员在类外可以直接被访问

protected和private修饰的成员在类外不能直接被访问。

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

C++中struct和class的区别

C++需要兼容C语言,所以C++中struct可以当成结构体去使用。

另外C++中struct也可以用来定义类, 和class是定义类是一样的

区别是struct的成员默认访问方式是public,class是的成员默认访问方式是 private。

  • 对成员变量而言定义是开辟一块空间(成员变量属于对象,用类去实例化对象时才会定义,不会单独定义,他是单独一个整体)
  • (定义与初始化、赋值无关 )
  • 对于成员函数而言定义就是函数的实现

类的实例化

用类类型创建对象的过程,称为类的实例化

  1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图,只设计出需要什 么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

面向对象的三大特性:封装、继承、多态。

在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。 封装本质上是一种管理,类也是一样,我们使用类数据和方法都封装到一起。 不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访 问。所以封装本质是一种管理。

封装:数据和方法都放到类中,可以访问的定义成公有,不能被访问的定义成私有

成员函数如果在类中定义,编译器可能会将当成内联函数,一般不是递归并且行数不多,会将其当作内联函数

规范建议:短小的函数在类里定义,作为内联,长的函数,声明和定义分离。

类对象的存储方式

成员函数和成员变量包含在一起

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多 个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

类的对象中只存在成员变量,成员函数存在公共代码区

在这里插入图片描述

类的大小

一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐。

注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

没有成员变量的类,他的对象需要给一字节进行占位,表示他的存在,这一字节不存储有效数据。

空类:定义这个空类去做某些标识,标识迭代器的类型。

仿函数:(不存储有效数据,在类中定义方法)。

隐含的This指针

(是个隐含的形参)

成员变量前可以自己加,也可以不加this指针,但是不可以在类的函数中的参数上加

在类中访问成员变量访问的不是类的,而是访问对象的,谁访问谁就是那个访问对象,(访问对象调用就把自己的地址传给了this指针,this指针再进行访问)(类没有空间,类只是声明

成员函数中,我们不加this指针,编译器会自己加,我们也可以显示的加上this

对象可以调用成员函数,成员函数中也可以调用成员函数,因为有this指针

this并不存在类中!!!this指针时隐含的并且存在栈区

this指针是形参,形参和函数中的局部变量是存在函数栈帧中的,所以this指针可以认为是存在栈区。

vs下this指针是通过ecx寄存器传递的。

空指针:空指针的访问是不会出问题的,空指针是个初始指针,这个指针不能被解引用访问,假设指针是编号,是第0个字节的地址,这个地址可以不被解引用访问。

class  A
{
public:
	void Display()
	{
		cout <<" Display() "<< endl;
	}
};
int main()
{
	A*p = nullptr;
	p->Display();
	return 0;
}

在这里插入图片描述

成员函数的地址不在对象中存储,而是存在公共代码段

Display不在p传给隐含的this指针指向的对象中,而是存在公共代码段中。那么这里调用成员函数Display,这里只会把p传给隐含的this指针。p被制空,p再传给this,this被制成空没有问题,并且也没有用this去解引用,也就不存在空指针解引用报错。

->是类或结构体等多个成员的指针,会拿这个指针去访问内部成员。

下面这段代码运行的结果

P传给print的隐含指针this,this去解引用访问成员变量了,就会造成空指针的访问的问题

  • 有空指针并没有错,主要是用空指针来访问会报错
class  A
{
public:
	void Print()//P传给print的隐含指针this,this去解引用访问成员变量了,就会造成空指针的访问的问题
	{
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A * p = nullptr;
	p->Print();
	return 0;
}

在这里插入图片描述

类的默认成员函数,在任何一个类中我们写的情况下都会自动生成默认成员函数

构造函数

构造函数不是创建对象,而是初始化对象。

  1. 函数名与类名相同
  2. 无返回值
  3. 对象实例化时自动调用对应的构造函数
  4. 构造函数可以重载(可以提供多种初始化对象的方式)

使用方法

传参调用、有参数的成员函数,没有参数、调用不带参的

参数不是在函数名后面,而是在对象后面 Ddte d2 (2021,1,1);

构造函数参数怎么写取决于你想怎么初始化

内置类型不会初始化

自定义类型会用编译器自己的无参构造函数进行初始化

  • 只要写了不管有没有参数都不会生成构造函数,没有写就用编译器自己的默认构造函数

默认构造函数: 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个.【例如全缺省和无参的构造函数,编译器不知道调用哪一个】

概括:不传参数就可以调用的就叫默认构造函数。(编译器默认生成的只是默认构造函数的一种),无参的和全缺省的都可以叫做默认构造函数

推荐写全缺省的构造函数,不传参数可以当作默认构造函数,传了可以自己定义

析构函数

析构函数是特殊的成员函数【自动调用】。

析构函数只是进行资源清理,并不是销毁,销毁是函数栈帧的功能。

其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值。
  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
  5. 【跟构造函数类似,内置类型不处理,自定义类型成员如果没有定义,会去调用编译器默认生成的析构函数去处理】
  • 如果引用传参,不是做输出型参数,最好用const & 来做保护
拷贝构造函数
  1. 拷贝构造的参数是同类型的

  2. 拷贝构造函数是构造函数的一种重载形式

  3. 拷贝构造的参数只有一个并且必须使用引用传参,使用传值调用会引发无穷递归调用

  4. 若未显示定义,系统默认生成拷贝构造【自定义类型和默认类型都处理了】

  5. 内置类型成员按内存存储字节序完成拷贝【又叫浅拷贝or值拷贝,就像memcpy全部拷贝】

  6. 自定义类型,会去调用自定义的拷贝构造函数完成拷贝

    如果本身就是一个拷贝构造,传值传参就又是一个拷贝构造,然后拷贝构造又得先传参,就会引发无穷递归调用

    拷贝构造也是一个构造函数,实例化一个对象时,会去调用构造函数,又因为在传参时参数传过去又是传的同一种类型,因此如果只是传值调用,就会一直创建构造函数无穷递归调用

运算符重载
赋值运算符重载

如果没有写编译器会生成默认赋值重载

跟拷贝构造类似,内置类型会完成值拷贝,自定义类型成员会调用他的赋值重载

运算符重载是我们自己去写一个函数去定义实现这里运算符的行为

运算符默认是给内置类型成员使用,自定义类型需要自己进行运算符重载

函数名:是operator+运算符 【operator==()】

参数:操作符有几个操作数,就有几个参数,参数类型是要进行操作对象的类型

返回值:看运算符返回后是什么

运算符可以写成全局的,但是面对私有的成员,有两种解决方案

  1. 直接定义在类里,变成成员函数
  2. 使用友元

这里将函数重载运算符定义在类里会出现错误

参数太多报错是由于在类中定义函数隐含的this指针

class Date
{
public:
	Date(int year = 0, int month = 1,int day = 1)//全缺省传参就用传的参数,没有就用自己里面自带的
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date &d1, const Date& d2)//不是做输出型的参数用const保护
	{
		return d1._year == d2._year
			&&d1._month == d2._month
			&&d1._day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

改进方式

	bool operator==( const Date& d)    
	{
		return _year == d._year
			&&_month == d._month
			&&_day == d._day;
	}
class Date
{
public:
	Date(int year = 0, int month = 1,int day = 1)//全缺省传参就用传的参数,没有就用自己里面自带的
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==( const Date& d)//不是做输出型的参数用const保护
	{
		//	return d1._year == d2._year
		//		&&d1._month == d2._month
		//		&&d1._day == d2._day;
		return _year == d._year
			&&_month == d._month
			&&_day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2021, 10, 10);
	Date d2(2020, 11, 11);
	//d1 > d2;
	//operator == (d1, d2);//这样也可以调用但是可读性很差,还不如直接写一个比较相等的函数
	d1 == d2;//这个是编译器自己转换成operator==(d1,d2)结果都是一样的

	d1 == d2;//编译器会自己去调用 d1.operator==(d2),一般不会显示的去调用
	d1.operator==(d2);//编译器对于自定义类型会采用这样的方式去调用
	return 0;
}
//在编译器中是这样转换调用的,汇编层面都是调用的同一个函数
//d1 == d2 -> d1.operator==(d2) -> d1.operator == (&d1,d2)
//bool operator==( Date * this,const Date& d)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JpPypYq1-1636256440990)(C:\Users\86181\AppData\Roaming\Typora\typora-user-images\image-20211026113334762.png)]

赋值运算符重载是用于两个已经定义出来的对象的拷贝复制;

拷贝构造是,一个对象准备定义,用另外一个对象来初始化构造他;

假设 d5 = d3 , d3已经创建出了实例化对象。虽然都是完成拷贝复制,虽然用了赋值符号但是这里是拷贝构造,只要去创建实例化对象都是构造,赋值重载是用于两个已经定义出来的对象

  1. Ddte & operator=(const & d)//这里使用了引用,减少了一层拷贝
    {
    	if(this != &d)
    	{
    	_year = d._year;
    	_month = d._month;
    	_day = d._day;
    	}
    	return * this;
    };
    
    int i = 1;
    int j = 2;
    int k = 3;
    
    i = j = k;
    
    void Printf()const  //这里的const后缀,在打印时面对const修饰的时候可以正常的打印,对于普通类型也可以正常使用
    {
    	cout<<_year<<"/"<<_month<<"/"<<_day<<endl;
    }
    
    

成员函数加const,变成const成员函数是有好处的。这样const对象可以调用,非const也可以调用

本质是保护指向的内容不能被修改

赋值表达式是要有返回值的,连续赋值要注意,这里是先把k赋给j;再把k付给j的表达式的返回值赋给i;

c++去实现数据结构类,都要自己写这几个成员函数,构造函数,析构函数,拷贝构造,赋值重载,都需要我们自己写,默认的函数达不到我们的预期。

总结拷贝构造和构造

在传值传参和传值返回中只要是在一个表达式调用连续步骤中,构造、 拷贝构造会被编译器优化合并,中间产生一个临时变量,优化后会直接跳过中间的步骤直接进行下一步

c++核心定义类+运算符重载

构造函数:先定义先调用

析构函数:后定义的先析构

静态的定义,静态区和全局对象都是在main函数销毁之后销毁的

多个全局对象,按照声明顺序来构造和析构

自定义类型对象作为独立对象时才会自己去调用构造函数,当作为子成员时独立对象的构造函数由独立成员的构造函数去调用的

自定义类型成员,改用初始化列表初始化在函数体内初始化,可以提高效率。

推荐使用初始化列表,不管是否使用初始化列表,对于自定义变量,还是会先使用初始化列表。

初始化列表可以视为单个成员一个一个定义的地方,尽管我们没有显示的写初始化列表,这里也是认为有初始化列表的,

经典例题:下面函数调用析构函数的顺序

C c;
int main ()
{
	A a;
	B b;
	static D d;  //第一次调用当前域函数的时候初始化,等到第二次在调用就不会被初始化了
	retuen 0
}
初始化列表

以一个冒号开始,接着以逗号分隔数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式

会使用默认初始化列表,也可以认为初始化列表是对象的成员变量定义的地方,成员变量整个对象实例化定义,单个成员的定义可以认为是在调用构造函数时初始化

所以尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化,索性不如直接使用初始化列表

有些成员是必须使用初始化列表初始化的

  1. const类型的成员必须在定义的时候初始化
  2. 引用也是必须在定义的时候初始化
  3. 没有默认构造函数的自定义类型成员

每个成员在初始化列表只能初始化一次

初始化列表和函数体内初始化,可以混合使用,互相配合

例如带头双向循环链表初始化时,两个成员都需要指向同一个成员赋值

List()
:_head(newNode(0))
{
_head->next = head;//有些初始化列表完成不了,所以该在函数内部初始化还在内部初始化,
_head->prev = head;
}
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

建议声明顺序和初始化顺序保持一致避免出现错误

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aKhHWR0w-1636256440991)(C:\Users\86181\AppData\Roaming\Typora\typora-user-images\image-20211103150828851.png)]

隐式类型转换:中间会产生一个临时变量,临时变量具有常性,需要修改要加个const


class A
{
public:
	A( int a)
		: _a(a)
	{
		cout << "A( int a)" << endl;
	}
	A(const A & aa)
	{
		cout << "A(const A & aa)" << endl;
	}
private:
	int _a;
};

int main()
{
	//隐式类型转换
	A aa1(1);
	A aa2 = 2;
	//先用2去构造一个临时对象,再用临时对象去拷贝构造aa2,最终编译器会优化,用2作为参数直接去构造aa2,
	
	return 0;
}

在这里插入图片描述

尽管结果一样都是调用了构造函数,但是aa2是编译器优化后的结果,在一个连续的表达式中,连续构造、拷贝构造、构造再拷贝,都会优化

static成员

static修饰初始化会在第一次调用当前域函数时被初始化

而存在静态区,属于整个类,也属于每个定义出来的对象共享,跟全局变量相比,他受类域和访问限定符限制,更好的体现封装性别人不能轻易的修改

class A
{
private:
	//声明
	int _a;	//存在定义的对象中,属于对象【对象实例化的时候才会定义,单独定义初始化的位置是在初始化列表】
	static int _count;存在静态区,属于整个类,也属于每个定义出来的对象共享
     //跟全局变量相比其受到类域和访问限定符的封装性,更好的收到保护
};
//定义初始化
//静态成员变量不能在构造函数中初始化,得在全局位置定义初始化
int A::_count = 0;

在类的外部不能够去访问私有成员函数,但是可以在类中定义一个公有的成员函数,再让公有成员函数去调用私有的

如果使用静态成员函数来调用突破类域的限制,也可以达到在类的外部访问调用成员函数

静态的都可以通过对象或者类指定的对象去访问

静态成员函数没有this指针因此静态成员函数不能访问非静态的成员

staic作用

  • c语言中可以修饰全局变量和全局函数,修改的是链接属性,只在当前作文件可见

  • 修饰局部变量,改变生命周期,本质是改变了数据的存储位置在C++中依旧有效

    C++

  • static又可以修饰成员变量和成员函数,成员变量属于整个类,为所有对象共享,成员函数没有this指针

  • 友元

    友元关系是单向的,不可以交换,不具有传递性

    A是B的友元,A可以的使用对象成员访问B的私有成员

    但是B不是A的友元,B不可以访问A的成员

    友元不宜多用,破坏了封装

内部类

两个类紧密关联的情况下使用

  1. 内部类可以定义在外部类的public、protected、private都是可以的。

  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

  3. sizeof(外部类)=外部类,和内部类没有任何关系。

  4. 内部类收到访问限定符和类域的限制

  5. 内部类天生就是友元

class A
{
private:
	static int k;
	int h;
public:
	class B //并且B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;
			cout << a.h << endl;
		}
	};
};
int A::k = 1;
int main()
{
	//B b;//不能直接实例化一个对象,需要指明类域
	A::B b;	//OK
	b.foo(A());
	cout << sizeof(A) << endl;//并且计算大小内部类也不算到其中

	return 0;
}

在这里插入图片描述

破坏了封装

内部类

两个类紧密关联的情况下使用

  1. 内部类可以定义在外部类的public、protected、private都是可以的。

  2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

  3. sizeof(外部类)=外部类,和内部类没有任何关系。

  4. 内部类收到访问限定符和类域的限制

  5. 内部类天生就是友元

class A
{
private:
	static int k;
	int h;
public:
	class B //并且B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;
			cout << a.h << endl;
		}
	};
};
int A::k = 1;
int main()
{
	//B b;//不能直接实例化一个对象,需要指明类域
	A::B b;	//OK
	b.foo(A());
	cout << sizeof(A) << endl;//并且计算大小内部类也不算到其中

	return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值