谈对象系列:C++类和对象

一、类的定义

1.1类定义的格式

使用class关键字,定义类,calssName是类名,{}中为类的主体,最后的分号 ;可别忘了加上。

类的名称,就是类的类型

  • 类定义的函数,属于inline内联函数,但具体展开还是得看编译器的选择。
class calssName
{
    //成员函数 
    
	//成员变量
};
int main()
{
    tag st1;//类名,就是类型
}

成员变量(member)的特殊标识:

  1. 在变量名前加上下划线
  2. 在变慢名前加上字母m

类的两种定义方法

将函数的定义和声明放在类里面实现:

这里使用class定义了一个日期类,在main函数里声明了一个day1对象,使用点操作符(.)、箭头操作符 (->)来访问类成员函数。

#include <iostream>
using std::cout;
using std::endl;

class Data
{
public:
	void Init(int year = 2024, int month = 6, int day = 6)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Data day1;
	day1.Init(2024, 5, 20); 
	day1.print();
	return 0;
}

将函数声明放在类里面,函数定义在类外面实现。

stack.h

#include <iostream>
class Stack
{
public:
	void Init(int capacity = 4);
	void Push(int x);
	void Pop();
	int Top();
	int Empty();
	void Destry()
	{
		assert(_next);
		if (_next)
			free(_next);
		_next = NULL;
		_top = 0;
		_capacity = 0;
	}
private:
	int _top;
	int _capacity;
	int* _next;
};

stack.cpp

void Stack::Init(int capacity)
{
	_next = (int*)malloc(sizeof(int) * capacity);
	_top = 0;
	_capacity = capacity;
}
void Stack::Push(int x)
{
	if (_top == _capacity)
	{
		int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
		int* newNext = (int*)realloc(_next, sizeof(int) * newcapacity);
		if (newNext == NULL)
		{
			perror("realloc fail");
			exit(1);
		}
		_next = newNext;
		_capacity = newcapacity;
	}
	_next[_top++] = x;
}

void Stack::Pop()
{
	assert(_next && _top > 0);
	_top--;
}
int Stack::Top()
{
	assert(_next && _top > 0);
	return _next[_top - 1];
}
int Stack::Empty()
{
	return _top == 0;
}

结构体:

在C++中兼容C语言,C++编译器也支持struct关键字,满足结构体的功能C,不仅如此C++还对它进行了升级,也支持类的定义格式。

这里在C语言中定义一个链表的结构的。

struct ListNode
{
	int val;
	struct ListNode* next;		
};

而C++兼容C语言,又对结构进行升级,在定义链表时可以直接省略struct关键字。

struct ListNode
{
	int val;
	ListNode* next;		
};

在结构体里和类一样还可以定义函数。下面使用C++实现栈。

struct Stack
{
public:
	void Init(int capacity = 4)
	{
		_next = (int*)malloc(sizeof(int) * capacity);
		_top = 0;
		_capacity = capacity;
	}
	void Push(int x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			int* newNext = (int*)realloc(_next, sizeof(int) * newcapacity);
			if (newNext == NULL)
			{
				perror("realloc fail");
				exit(1);
			}
			_next = newNext;
			_capacity = newcapacity;
		}
		_next[_top++] = x;
	}
	void Pop()
	{
		assert(_next && _top > 0);
		_top--;
	}
	int Top()
	{
		assert(_next && _top > 0);
		return _next[_top - 1];
	}
	int Empty()
	{
		return _top == 0;
	}
	void Destry()
	{
		assert(_next);
		if (_next)
			free(_next);
		_next = NULL;
		_top = 0;
		_capacity = 0;
	}
private:
	int _top;
	int _capacity;
	int* _next;
};
int main()
{
	Stack s1;
	s1.Init();

	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);

	while (!s1.Empty())
	{
		cout << s1.Top() << endl;
		s1.Pop();
	}
	s1.Destry();
	return 0;
}
  • 可以发现使用C++实现的栈,与C语言没有过多的区别,只是将栈的接口放在了结构体内部。

  • 而在使用栈的结构体时,并不像C语言那样需要频繁的传地址,只需调用类成员函数即可,以及在每一个函数名前省略了Stack,栈的函数是在结构体内实现的,属于Stack类域,即使是其它地方的函数实现了,Init、Push、Pop等功能,那也是支持的。

  • 调用栈的接口函数需使用 点操作符(.)

1.2访问限定符

可以发现,在上述实现类中,使用了 pulicprivate这两个是访问限定符,用于限定对类成员访问的权限

  • C++将对象的属性与方法封装在一起,通过访问权限选择性的将接口提供给外部使用。
  • public公有的,被public修饰的成员在类外可以被访问,private和protected修饰的成员不能在类外部直接访问,它两的效果差不多,常用 private,两者之间的区别要在继承中才能体现出。
  • 访问限定符的作用域,从限定符开始知道遇见下一个限定符结束,或者遇见 }结束。
  • class定义的成员没有使用访问限定符修饰,class内默认使用 private修饰,struct内默认使用 pulic修饰。
    • 被限制在了类中,而不能在外部使用相当于一种保护,是不希望类中被🔒锁上的成员被修改。一般不提供外部使用的成员变量使用 private和protected修饰,提供外部使用的成员函数使用 pulic修饰。

1.3类域

类定义了一个新的作用域,类域影响了编译器的查找规则,类的所有成员都在类域中,想要访问类域的成员使用 ::域作用限定符指出成员属于哪个类

如同上述的将函数声明放在类里面,函数定义在类外面实现。想要在类外实现类函数的定义,就必须使用上::域作用限定符

使用 Stack::,Stack指明了Init函数属于Stack类域

void Stack::Init(int capacity)
{
	_next = (int*)malloc(sizeof(int) * capacity);
	_top = 0;
	_capacity = capacity;
}

声明和定义分离,需要指定类域

二、实例化

2.1变量的声明和定义

变量的声明是不占空间内存的,只是告诉编译器存在这么个东西;而定义是在内存中开辟空间。

以下变量是声明还是定义?

class Data
{
	int _year;//声明
	int _month;//声明
	int _day;//声明
};
int val;//定义
int main()
{
    int n;//定义
    return 0;
}

声明就相当于别人给你的口头承诺,至于会不会实现那是另一回事,只是他告诉你有这么个事,而定义就相当于别人实实在在的帮你做完了事,不局限于声明。


对象的实例化

对象实例化,就是使用类类型创建变量的过程,类的成员变量只是一个声明,还没有为其开辟空间,类是对对象的一个描述,需要那些功能,那些变量实现一个对象,就会在类中一一实现。

就好比如一个塑料瓶子,有了一个瓶子的模型,需要大量生成塑料瓶的时候都会按照这个模型生产。类和对象也如同这样,使用类将一个需要描述的对象的功能,框架等一一在类中实现,在需要使用的时候使用类创建一个对象。

同理一个类可以实例化多个对象,就像一个冰棍的模型可以生产许多颗冰棍。实例化的对象会占据实际的物理空间,存储数据。

2.2类的大小

该如何去计算一个类的大小,它实际上与之前学习过的计算结构体大小规则一致。

结构体内存对齐的原则:

  • 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处(上述两个结构体c1偏移量为0的原因)

  • 其它成员变量要对齐到对齐数的整数倍的地址处

    • 对齐数:编译器默认的第一个对齐数 与 该成员变量大小的较小值。
    • vs中默认—8
    • linux中gcc 没有默认对齐数,对齐数就是成员自身大小。
  • 结构体中每个成员变量都有一个对齐数。

  • 结构体总大小为最大对齐数(结构体中最大对齐数)的整数倍。

  • 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员中的对齐数)的整数倍。

那,咱就使用对齐规则尝试计算类的大小

class Data
{
	int _year;
	int _month;
	int _day;
};

在这里插入图片描述

可以发现,使用对齐规则计算后的大小为12个字节


但类中是有函数的定义,计算类的大小时需要将类成员函数也一起计算吗?事实上并不需要。

#include <iostream>
using std::cout;
using std::endl;

class Data
{
public:
	void Init(int year = 2024, int month = 6, int day = 6)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	printf("%zd\n", sizeof(Data));
	return 0;
}

在这里插入图片描述

可以发现,在类中定义了两个类成员函数后,计算的大小还是12个字节,符合上述通过对齐规则计算后的结果。

类没有为类成员函数开辟内存空间,那它们是存储在哪里。

类成员函数与类成员变量分开存储的。在计算类的大小是只考虑成员变量。使用同一个类实例化的对象,对应每一个对象的成员变量来说都是存放在不同的空间中来管理数据,而每一个对象的成员函数 例如Init,都需调用相同的函数,没有必要为每一个对象都开辟空间存放 Init函数,这样会造成大量的空间浪费。

成员函数都有一块公共区域来存放,对于每一个对象来说是公有的,大家可以一块使用。


计算空类的大小(面试):

class S{};

计算一个空类,它的大小肯定不会为0

int main()
{
	S s;
	printf("%zd\n", sizeof(s));
	return 0;
}

在这里插入图片描述

在main函数里定义了类S对象s,它是通过类的大小开辟空间,若一个空类的大小为0,定义空类的时候编译器就会报错。

编译器给空类一个字节类唯一标识这个类的对象这一个字节不存储有效数据,为一个占位符,标识对象被实例化。

三、this指针

Date类中有Init和print两个成员函数,函数体内没有为不同对象进行区别,在使用day1、day2调用 Init函数时,函数是如何区别是day1还是day2的。这里就通过C++提供的隐含的this指针来解决。

  • 编译器编译后,类的成员函数默认都会在形参的第一个位置,增加一个当前类类型的指针,叫this指针。Date类中实现的Init函数原型为 void Init(Data* const this, int year int month, int day)
  • this作为一个关键字存在,不能显示的写在形参中,编译器会自动完成。规定
  • 在函数内可以显示的用this指针,可以给它加上,也可以不加。不添加并不代表不存在this指针
  • 外界无法传入当前对象的地址给到被调用的成员函数,使用this指针接受当前对象的地址,,this指针就会通过不同的地址去找到内存中对应的成员变量。
  • this指针只能在成员函数内部使用,因为它是作为一个成员函数的形参,若是没有传递给当前对象地址的话,那么它的指向是不确定的
  • this指针作为函数参数,是不会存储在类中,而是随着函数栈帧的开辟,存放在栈区。
class Date
{
public:
   //void Init(Data* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
        //this->_year = year;
		//this->_month = month;
		//this->_day = day;
	}
    void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date day1;
	Date day2;
	day1.Init(2024, 5, 20); 
	day1.print();
	return 0;
}

在这里插入图片描述
在这里插入图片描述

通过编译器调试,可以发现的确存在this指针来接收了day1传递的地址。


小考题

下面程序编译运行后的结果是:

A、编译报错 B、运行崩溃 C、正常运行

#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << "A::Print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

答案是B,主函数内部不存在空指针的解引用,Print不存在对象里面,是在一块公共区域,若 p->_a这样写,就会出现空指针的解引用, _a是存在对象内部的,就像结构体里空链表对结构体成员的访问。

  • 成员函数的指针是在编译时确定的,没有存放在对象中,虽然写成了p->Print(),但是没有解引用。

问题出在Print函数内,Print函数的this指针接收了传递过来的空指针,而又使用 this->_a,对空指针进行了解引用,从而造成程序崩溃。

评论 72
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值