【C++】_2.类和对象(1)

目录

1.面向过程与面向对象

2.类的引入

2.1 类的形式

2.2 C++将struct升级为类

3.类的定义

4. 类的访问限定符及封装

4.1 访问限定符

4.2 封装

5.类的作用域

6.类的实例化

7.类对象模型

7.1 类对象存储方式

7.2 内存对齐规则与类对象的大小

8.this指针


正文:

1.面向过程与面向对象

C语言时面向过程的,分析求解问题的步骤通过函数调用逐步解决问题;

C++是基于面向对象的,将一件事拆分为不同的对象,靠对象之间的交互完成。

注意:C++不是纯面向对象的,兼容C语言的情况下,是面向对象与面向过程混编的。

2.类的引入

2.1 类的形式

以栈为例:

(1)在C语言中,定义栈及其部分相关函数为:

struct Stack_C
{
int* a;
int top;
int capacity;
}
void Stack_CInit(struct Stack_C* ps);
void Stack_CPush(struct Stack_C* ps,int x);
void Stack_CPop(struct Stack_CPop* ps);
int main()
{
struct Stack_C sl1;
Stack_CInit(&sl1);
Stack_CPush(&sl1,1);
Stack_CPush(&sl1,2);
Stack_CPush(&sl1,3);
Stack_CPop(&sl);
return 0;
}

(2)在C++中,将struct升级为类后,其包含成员变量成员函数两部分,定义栈及其部分相关函数为:

struct Stack
{
//成员函数
void Init()
{//...}
void Push(int x)
{//...}
void Pop()
{//...}
//成员变量
int* a;
int top;
int capacity;
}
int main()
{
struct Stack st1;//兼容C语言
Stack st2;//省去struct
st2.Init();
st2.Push(1);
st2.Push(2);
st2.Push(3);
st2.Pop();
return 0;
}

2.2 C++将struct升级为类

以链表为例:

(1)在C语言中,结点定义为:

typedef struct ListNode_C
{
struct ListNode_C* next;
int val;
}ListNode_C;

① 在typedef之前,即在结点定义结构体内部还不可以使用typedef后的名字,还须使用struct ListNode全称;

② struct不能省略;

(2)在C++中,将struct升级为类,ListNode既可以表示类名,也是类型,故而可以省略struct,同时也省去了typedef的麻烦:

struct ListNode_Cpp
{
ListNode_Cpp* next;
int val;
}

PS:关于结构体的定义,C++更喜欢用class来代替。

3.类的定义

class ClassName
{
//类体:成员变量与成员函数
};

class为定义类的关键字,ClassName为类名,{}中为类的主体,定义类结束后须有分号。

类中的元素称为类的成员:

类中的数据称为类的属性或成员变量;类中的函数称为类的方法或成员函数。

类的两种定义方式:

1.声明和定义全部放置在类体内:

注意:成员函数如果在类中定义,如果符合inline条件(函数短小)编译器可能会将其当成内联函数处理;比如:

struct Stack
{
void Init()
{
a=0;
top=capacity=0;
}
void Push(int x)
{//...}
int* a;
int top;
int capacity;
}
int main()
{
Stack st;
st.Init();
st.Push(1);
return 0;
}

在上述代码中,Init函数代码短小,写在类体内就会被当做内联函数展开; 

2.声明放置于.h中,定义放置于.cpp中:

//.h
struct QueueNode
{
QueueNode* next;
int val;
};
class Queue
{
public:
void Init();
void Push(int x);
void Pop();
private:
QueueNode* head;
QueueNode* tail;
};

//.cpp
Queue::Init()
{//...}
Queue::Push(int x)
{//...}
Queue::Pop()
{//...}

调试进入反汇编:

可以发现,Init函数并非内联函数,即使定义时inline修饰也不可令其变为内联函数,因为内联函数不支持定义与声明分离;

总结:类的定义:

1.小函数要成为内联函数直接在类体内定义即可;

2.大函数应该声明与定义分离;

PS:类成员变量命名规则的建议:

当正常命名如下时:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		year = year;
        month=month;
        day=day;
	}
private:
	int year; // 年
	int month; // 月
	int day; // 日
};

成员变量与函数形参的指代就会容易混淆,故而建议采用:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

 常见命名规则方法有:
① 单词和单词之间首字母大写间隔,如:GetYear;(驼峰法)

② 单词全部小写,单词之间用下划线_分割,如:get_year;

建议:a.函数名、类名等所有单词首字母大写;如:DateMgr;

b.变量首字母小写,后面单词首字母大写;如:datemgr;

c.成员变量首单词前加上_;如:_datemgr;

4. 类的访问限定符及封装

4.1 访问限定符

C++实现封装的方式:用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性地将其接口提供给外部的用户使用。

 访问限定符的说明:

1.public修饰的成员在类外可以直接访问,也能在类内访问;

2.protected和private修饰的成员在类外不能直接被访问;(现阶段两限定符等价,继承后区别)

3.访问权限作用域从该访问限定符出现的位置到下一个访问限定符出现时为止;

4.class的默认访问权限为private,struct为public(因为struct需要兼容C语言);

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

如:

class  Stack
{
public:
	void Init()
	{
		a = 0;
		top = capacity = 0;
	}
	void Push(int x)
	{
		//...
	}
private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack st;
	st.Init();
	st.Push(1);
	st.Push(2);
	st.Push(3);
    
    st.a=nullptr;
	return 0;
}

运行如上代码,编译器会报错:

4.2 封装

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

在类和对象阶段,只关注封装。

封装是指将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节(private和protected修饰),仅对外公开接口(public修饰)来和对象进行交互,本质上是一种管理。

一般情况下,成员变量一般会私有或保护,成员函数一般会公开

C语言不支持封装,可以使用函数访问数据,也可以直接访问数据,并不规范且易出错;

C++支持封装,只能通过成员函数访问成员变量,避免了直接访问成员变量时访问错误。

封装的本质是一种管理;

5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。

在类体外定义成员时,需使用::作用域操作符指明成员属于哪个类域。比如:

class Person
{
public:
 void PrintPersonInfo();
private:
 char _name[20];
 char _gender[3];
 int _age;
};
// 需指定PrintPersonInfo属于Person类域
void Person::PrintPersonInfo()
{
 cout << _name << " "<< _gender << " " << _age << endl; }

当PrintPersonInfo函数体内没有与类体内私有封装的变量有关的运算时,即使并未限定PrintPersonInfo为Person::PrintPersonInfo,系统也不会报错,因为在不同的类域中允许同名函数的存在。 

PS:

 即:作用域与编译器的搜索规则有关,而生命周期与存储的区域有关,二者不能混为一谈

6.类的实例化

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

仍以上文提及的某一代码为例解释类的实例化:

//Person.h

#pragma once
#include<iostream>
using namespace std;
int age;//变量的定义
//全局变量,已开空间
class Person
{
public:
	void PrintPersonInfo();      //函数的声明
private:
	char _name[20];             //变量的声明
	char _gender[3];
	int _age;
	//三个变量并未开空间
};


//Person.cpp

#include"Person.h"
void Person::PrintPersonInfo()   //函数的定义
{
	cout << _name << " " << _gender << " " << _age << endl;
}


//Test.cpp

#include"Person.h"
int main()
{
	cout << sizeof(Person) << endl;
	//当给类开辟空间时,仍然可以计算类的大小;
	Person p1;//此时才为结构体内的成员变量开空间
	//定义对象的过程称为类的实例化

	return 0;
}

对于函数来说,仅声明函数名与形参即是函数的声明,带有函数体的功能实现即是函数的定义;

对于变量来说,未给变量开辟空间即是变量的声明,已给变量开辟空间即是变量的定义

PS:(1)类像设计图,只需要设计出概念图,而类实例化出对象就像依照设计图进行实体建筑。实例化出的对象才能实际存储数据,占用物理空间。

(2)上述代码中,会出现重定义的问题,这是因为Person.cpp与Test.cpp都包含Person.h,最后生成.o文件时会进行文件合并,此时就会发生定义的冲突。所以.h文件中谨慎定义全局变量。

修正方法为:

方法一:将 int age修改为extern  int  age,从而将变量的定义改为声明,此时在.cpp文件中就可以进行定义,即:int age=0; 从而将变量的声明与定义分离,在Person.cpp文件中,全局变量age既有声明,也有定义,在Test.cpp文件中,只有全局变量的声明,也可以进行使用;

方法二:将int age修改为static int age,修改链接属性,int age是所有文件可见,static int age仅是当前文件可见,其连接属性不放入其他文件的符号表,故而此种情况下,Person.cpp与Test.cpp文件中包含的age是不一样的,但是都能使用.h文件中的size是因为引头文件后在.h文件的符号表中寻找得到。

为方便起见,重新定义静态全局变量a:

//Person.h

#pragma once
#include<iostream>
using namespace std;
//int age;//变量的定义
//全局变量,已开空间
static int a;//仅在当前文件可见
//extern int age;
class Person
{
public:
	void PrintPersonInfo();      //函数的声明
private:
	char _name[20];             //变量的声明
	char _gender[3];
	int _age;
	//三个变量并未开空间
};

//Person.cpp

#include"Person.h"
void Person::PrintPersonInfo()   //函数的定义
{
	//cout << "static size:" << &size << endl;
	//cout << _name << " " << _gender << " " << _age << endl;
	cout << "a:" << &a << endl;
}

//Test.cpp

#include "Person.h"
int  main()
{
	cout << "a:" << &a << endl;
	//cout << "static size:" << &size << endl;
	//cout << sizeof(Person) << endl;
	//当给类开辟空间时,仍然可以计算类的大小;
	Person p1;//此时才为结构体内的成员变量开空间
	//定义对象的过程称为类的实例化
	p1.PrintPersonInfo();
	return 0;
}

运行代码打印两个文件中的a的地址:

二者地址不同,证明两个文件中的a并不相同,印证了static修饰的全局变量尽在当前文件可见; 

7.类对象模型

7.1 类对象存储方式

猜想一:对象中包含类的各个成员:

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

猜想二:代码只保存一份,在对象中保存存放代码的地址:

此种方法在此处未被采用,后续详谈;

猜想三:只保存成员变量,成员函数存放在公共的代码段:

class A 
{
public:
	void func ()
	{
		cout << "void A::func()" << endl;
	}
	char _a;
};
int main()
{
	A* ptr = nullptr;
	ptr->func();
	return 0;
}

若采取前两种存储方法,则程序崩溃,而该程序正常运行,说明调用func函数并未根据变量取寻找地址进行解引用,而是在编译链接时就根据函数名去公共代码区找到函数地址,再call函数地址;

7.2 内存对齐规则与类对象的大小

//类中既有成员变量又有成员函数
class A1
{
public:
	void PrintA()
	{
		cout << _a << endl;
	}
	char _a;
	int _i;
};
// 类中仅有成员函数
class A2
 {
public:
 void f2()
 {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
	return 0;
}

结合7.2相关知识可知,成员函数不进对象,即不占对象空间的大小,其余遵循C语言结构体内存对齐规则,易知上述代码的运行结果为8  1  1 (没有成员变量的类的大小都为1:没有成员变量的类对象,给1byte用于占位,标识对象存在,不存储实际数据);

C语言结构体内存对齐详见C语言专栏自定义类型:链接:CSDN

8.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;
	d1.Init(2022, 10, 10);

	Date d2;
	d2.Init(1997, 8, 5);

	d1.Print();
	d2.Print();
	return 0;
}

实际上编译器会处理为:

class Date
{
public:
	void Init(Date* const this,int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print(Date* const this)
	{
		cout << this->_year << "-" <<this->_month << "-" << this->_day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	d1.Init(&d1,2022, 10, 10);

	Date d2;
	d2.Init(&d2,1997, 8, 5);

	d1.Print(&d1);
	d2.Print(&d2);
	return 0;
}

图示:

 底层逻辑是编译器会处理为含有this指针的代码,但是在我们编写时,不能显示this指针作为参数进行传递和接收,在成员函数体内部可以进行this指向,同时this指针是默认const修饰的,不可以进行修改,但是this指针指向的内容可以进行修改。(代码中this可全部注明、也可部分注明或全不注明)

示例:

class A
{
public:
	void PrintA()
	{
		cout << this << endl;
		cout << _a << endl;
	}
	void Print()
	{
		cout << this << endl;
		cout << "Print()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	//p->Print();//正常运行
	p->PrintA();//运行崩溃
	return 0;
}

在上述代码中,当类对象为空指针时,p->Print可以正常运行是因为将p传递给Print函数后并未对p进行使用,而p->PrintA函数运行崩溃是因为p传递给PrintA函数后使用了p,this为空指针,则_a链接错误;

PS:一般情况下,this作为一个形参,存在于栈区;但在少数进行优化的情况下,this也会存在于其他区域(取决于编译器):

如:

class A
{
public:
void PrintA(int x)
{
cout<<this<<endl;
cout<<"PrintA()"<<endl;
}
private:
int _a;
}
int main()
{
A aa;
aa.PrintA(1);
return 0;
}

调试查看反汇编:

VS并未将this指针放置在栈区上,而是进行了优化,将aa的地址放置在ec寄存器中,速度更快。

即VS通过ecx寄存器传递this指针。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值