C++之二:类和对象

相关代码:

C语言是面向过程的,关注的是过程,分析求解问题的步骤,调用函数逐步解决问题。

C++是面向对象的,关注的是对象,将一件事情的完成分为不同的几个参与者(对象),靠参与者的交互来完成任务。

一、类

C语言中,结构体只能用来定义变量;在C++中,结构体不仅可以定义变量,还可以定义函数

C++中更常用class来定义类

类体中内容称为类的成员:

  • 类中的变量称为类的属性或成员变量
  • 类中的函数称为类的方法或者成员函数

二、类的定义

类的两种定义方式:

1、声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

2、 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

3、定义一个类

cpp 复制代码
// 定义一个数据结构栈的类
const int N = 10;
class Stack {
public:
    void Init() {
        _a = (int*)malloc(sizeof(int) * N);
        _top = 0;
        _capacity = N;
    }
    void Destory() {
        free(_a);
        _a = NULL;
        _top = -1;
        _capacity = 0;
    }
    void Push(int x) {
        if (_top == _capacity) {
            _capacity *= 2;
            int* tmp = (int*)realloc(_a, sizeof(int) * _capacity);
            if (tmp != nullptr) {
                _a = tmp;
            }
        }
        _a[++_top] = x;
    }
    void Pop() { _top--; }
    int Top() { return _a[_top]; }

private:
    int* _a;
    int _top;
    int _capacity;
};

三、类的实例化

1、访问限定符

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

  1. public修饰的成员在类外可以直接被访问。
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)。
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
  4. 如果后面没有访问限定符,作用域就到 } 即类结束。
  5. class的默认访问权限为private,struct为public(因为struct要兼容C)。

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

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

A:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。(C++的struct中也可以定义函数,C中的struct不能定义函数。C++中class和struct唯一的区别就是默认权限的不同)。

2、封装

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

封装 :将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

3、类实例化对象

类实例化对象,相当于定义出了类的成员变量。

类,本质上就是一种自定义类型以及相应的操作函数)。

cpp 复制代码
int main()
{
	//类实例化对象
	Stack st;
	st.Init();
	st.Push(1);
	cout << st.Top() << endl;
	st.Pop();
	st.Destory();
	return 0;
}
4、类的大小

与C语言结构体类似,存在内存对齐(不了解的可看结构体那一章,内存对齐的规则)。但是类实例化的对象的大小只与成员变量有关,不存放函数。

cpp 复制代码
	cout << sizeof(st) << endl;  //12

上述Stack类对象的大小占12Byte,这就说明,一个类实例化出的对象并没有存储成员函数。

原因:

1、一个类实例化出N个对象,每个对象的成员变量存储的值可以是不同的。

2、但是调用的函数是相同的代码

3、如果每个对象都存放成员函数,而这些成员函数是相同的。那么创建大量对象时,会浪费大量的空间。

4、因此:对象中只存放成员变量,而成员函数存放在公共代码段

没有成员变量的类大小是1Byte

cpp 复制代码
class A2
{
public:
	void A_2()
	{
	}
};
class A3
{
};

Q:问什么没有成员变量的类的大小是1而不是0?

A:假设没有成员变量的类的大小是0。则当该类实例化一个对象,调用成员函数时,找不到实例化对象的地址,无法调用函数。因此,系统规定,没有成员变量的类的大小是1。

5、this指针

隐含的this指针:C++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数。

  1. 编译器让该指针指向当前对象,在函数体中所有"成员变量"的操作,都是通过该指针去访问。
  2. 只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
  3. this指针一定加载函数参数的第一个位置。(用户加不加都可以)。

怎么证明this指针确实存在?

对于一个成员函数,在该函数内部,当我们调用相应的成员变量时,我们没有传入相应参数,而且成员函数与成员变量没有存放在同一区域,那我们为什么可以调用该成员变量呢?

有关this指针的一个问题:

cpp 复制代码
//下面的代码可以正常运行吗?
class A4
{
public:
	void Print()
	{
		cout << /*this->*/_a << endl;//当传入this==nullptr时,产生访问冲突
	}
	void Show()
	{
		cout << "show()" << endl;
	}
private:
	int _a;
};

int main()
{
	A4* p = nullptr;
	p->Show();	//正常运行
	p->Print();//编译能通过,但是程序崩溃,原因:访问空指针
}
// Show(A4* this);Print(A4* this)
// p->Show()  -->  Show(p);
//p->Print()  -->  Print(p);要访问空指针,

四、类的默认函数

默认成员函数:当用户没有实现时,编译器会自动生成的。(包括默认构造函数、默认析构函数)

1、构造函数

构造函数 ,在创建对象时调用的函数,完成初始化工作。(初始化对象)。

  1. 函数名与类名相同。
  2. 无返回值。(返回void也不行)。
  3. 对象实例化时编译器自动调用对应的构造函数。(实例化时自动调用)。
  4. 构造函数可以重载
  5. 如果没有构造函数,编译器会自动生成一个无参的默认构造函数。(显式定义后编译器不在生成)。
  6. 编译器自动生成的只会初始化自定义类型成员变量。(C++11在这里又有所修改)
  7. 无参的构造函数与全缺省的构造函数都称为默认构造函数 ,而默认构造函数只能有一个。(全缺省构造函数和无参构造函数不能同时存在)。
cpp 复制代码
class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};


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;
	Time _t;// 当Date没有构造函数时,会对Time调用构造函数初始化,对int型不初始化
};

int main()
{
	Date d1;//调用编译器生成的构造函数
	d1.Print();
    return 0;
}
cpp 复制代码
//我们没有显式定义构造函数,这里编译器会生成无参的默认构造函数
	//默认的构造函数:
	// 1、针对内置类型的成员变量没有初始化
	// 2、对自定义类型的成员变量,调用它的构造函数初始化 
	
	//构造函数,在对象构造时调用的函数,完成初始化工作
	/*Date()
	{
		//全缺省的构造函数和无参默认构造函数不能同时存在:存在歧义
		_year = 0;
		_month = 0;
		_day = 0;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	Date(int year = 0, int month = 0, int day = 0)
		//更好的书写构造函数方式    -->    全缺省
	{
		_year = year;
		_month = month;
		_day = day;

	}

默认构造函数有三个无参构造函数、全缺省的构造函数、编译器自动生成的

三者共同的特点是:不需要传入参数就可以初始化

2、析构函数

析构函数:对象生命周期到了以后,自动调用。

完成对象里面的资源清理工作,不是对d1和d2的销毁。

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 对于没有开辟动态内存的类,析构函数写不写都可以。
  6. 与构造函数类似,当使用编译器自动生成的析构函数时,只会析构自定义成员变量。

Q:为什么说析构函数是资源清理而不是销毁对象?

A:当成员变量中存在动态开辟的内存时,调用析构函数会释放这片空间,但是对象仍然存在,析构函数调用完后,对象的生命周期走到尽头,会被系统自动销毁。

cpp 复制代码
	//析构函数:对象声明周期到了以后,自动调用。
	//完成对象里面的资源清理工作,不是对d1和d2的销毁
	~Date()
	{
		//没有参数也没有返回值
		cout << "~Date()" << endl;
	}

3、拷贝构造函数

拷贝构造函数:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个 且必须是类实例化对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

了解引用之后,要记得C++对于传参有三种形式:

1、传值,(最浪费空间,而且不能修改原值)。

2、传址,(占用的空间与CPU位数有关,解引用修改原值)。

3、传引用,(不开辟空间,对引用的操作直接作用在原值)。

cpp 复制代码
//const保证不通过d来修改d1
Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
Date d1(17,5,6);
Date d2(d1); //等价于d2 = d1 ,即拷贝构造函数
Date d3 = d1;

4、运算符重载函数

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数

也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名:关键字operator后面接需要重载的运算符符号

相关推荐
kiiila17 分钟前
【Qt 常用控件】按钮类(QPushButton、QRadioButton、QCheckBox)
开发语言·qt
江木12343 分钟前
Python Numba多流和共享内存CUDA优化技术学习记录
开发语言·python·学习
千里马学框架1 小时前
安卓java端service如何在native进程进行访问-跨进程通讯高端知识
android·java·开发语言·安卓framework开发·车机·跨进程·安卓窗口系统
NULL->NEXT1 小时前
Java(面向对象进阶——接口)
android·java·开发语言
技术的探险家1 小时前
R语言的文件操作
开发语言·后端·golang
珹洺2 小时前
音乐播放器实现:前端HTML,CSS,JavaScript综合大项目
开发语言·前端·javascript·css·gitee·bootstrap·html
多多*2 小时前
Sync底层字节码 monitorEnter和monitorExit 对象监视器
java·开发语言·windows·python·spring
爱上语文2 小时前
MyBatis实现数据库的CRUD
java·开发语言·数据库·mybatis
qq_433618442 小时前
c++ chrono 操作 以及c语言的时间操作
开发语言·c++
BinaryBardC2 小时前
Go语言的文件操作
开发语言·后端·golang