【C++】类和对象

经过前面的铺垫学习,终于来到了新的篇章,可以说几乎所有我们前面所学都是为了类和对象的铺垫

this指针

class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << _month << _day << endl;
	}

private:
	int _day;
	int _month;
	int _year;
};
int main()
{
	Date d1;
	Date d2;
	d1.Print();
	d2.Print();

	return 0;
}

经过前面的学习我们知道类的函数是放在公共代码区段的,那么Print函数是怎么区分是哪个对象调用它的呢,这不禁令人好奇?

C++中通过引入this指针解决该问题

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

 【C++】类和对象_第1张图片

简单来说就是函数其实是有参数的,为(类的名称)*的指针,不过是编译器把它藏起来了,看上去更方便高级了

this指针的特性

  • this指针的类型:类类型* const,即成员函数中,不能给this指针赋值
  •  只能在“成员函数”的内部使用
  • this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
  • this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
  • 非静态成员函数的第一个参数就是隐藏的this指针
  • this指针代表了当前对象,能够区分每个对象的自身数据
  • 静态成员函数没有this指针,只有非静态成员函数才有this指针,且为隐藏指针

类的六个默认成员函数之构造函数

大家在使用各种数据容器的时候,第一步往往都是先初始化对象,然后再进行操作,但是这一步往往很容易被我们忽略,从而导致后续的操作发生错。针对这种情况,c++发明了构造函数这一语法

注意构造函数并不是创建一个对象,而是进行对象的初始化工作

构造函数没有返回类型,名字和类名相同,是一个特殊的成员函数,有很多特殊麻烦的规定,大家不要觉得按照自己的设计思路来感觉会更好,因为我们是来学习语言的,不是来创造语言的
 

构造函数的几大注意事项

  • 1.构造函数是自动调用的,无需我们进行手动调用操作
  • 2.如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
  • 3.构造函数可以重载
以下代码包含上面三种情况,大家可以拿去试验一下,加深印象

class Date
{
public:
	Date()
	{
		cout << "构造函数1的调用" << endl;
	}
	Date(int year, int month, int day)
	{
		cout << "构造函数2的调用" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;//调用无参构造函数

	Date d2();
	 //注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,这里会报错
	
	Date d3(2023, 7, 22);
	//调用有参构造函数的时候,在创建对象的时候后面就要加括号

	return 0;
}
  • 4.如果类中没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数,一旦显示定义编译器将不再自动生成
  • 5.无参构造函数,编译器自动生成的函数和全缺省构造函数都称为默认构造函数
  • 6.编译器生成的默认构造函数不操作内置值,使大部分人都觉得默认构造函数没什么用,针对这一情况后续c++也作出了调整——内置类型成员(int ,char,任何类型的指针.......)可以在声明的时候给默认值
  • 7.如果给内置类型成员声明的时候给了默认值,又自己写了默认构造函数,那么会先给内置成员类型成员赋值,然后再去走自己定义的默认构造函数,如果默认构造函数修改了内置值,那么内置成员对象的值就为修改后的,否则内置成员对象的值就是默认值。
  • 8.大多数类的构造函数和析构函数都要自己去实现,自定义类型的类大多可以不用自己显示定义(例如下面的Date类型里的Time),本质上还是自动调用构造函数,c++想尽可能减少一些代码量,并不是编译器自动帮你初始化,如果是这样那也就不需要程序员的存在了
  • 9.构造函数的调用也是按照栈的规则——先进后出,后进先出来进行调用的

以下是6-8情况的代码,大家可以拿去试验一下

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	//Date()
	//{
	//	_year=2023;
	//	_month = 7;
	//	_day = 22;
	//}

	Date(int year = 2019, int month = 9, int day = 1)
	{
		_year = 2019;
		_month = 9;
		_day = 1;
	}
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

类的六个默认成员函数之析构函数

有了初始化,那自然就有销毁的过程啦 ,但是我们知道局部变量在出了栈区后会自动销毁,而堆区里的数据却不会,所以堆区的数据往往存在内存泄漏的问题

针对后面的情况,c++提出了析构函数来自动销毁堆区的数据,如果类的成员都是栈区的局部变量,那就没有写析构函数的必要了

析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

析构函数是特殊的成员函数,其特征如下:

  • 1. 析构函数名是在类名前加上字符 ~。
  • 2. 无参数无返回值类型。
  • 3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  • 4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
class Stack
{
public:

	Stack(size_t n=4)
	{
		cout << "构造函数的调用" << endl;

		if (n == 0)
		{
			_a = nullptr;
			_top = _capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int) * n);
			_capacity = n;
			_top = 0;
		}
	}

	~Stack()
	{
		cout << "析构函数的调用" << endl;
		free(_a);
		_a = nullptr;
		_capacity = _top = 0;
	}

	void Push(int x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : (2 * _capacity);
			int* tmp=(int*)realloc(_a,sizeof(int) * newcapacity);

			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}

			if (tmp == _a)
			{
				cout << "原地扩容" << endl;
			}
			else
			{
				cout << "异地扩容" << endl;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}

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

int main()
{
	Stack st;
	st.Push(1);
	st.Push(2);
	st.Push(3);

	return 0;
}

可能大家觉得析构函数并没有什么用,但它其实可以解决c语言解决不了的一些问题

括号匹配问题是之前数据结构学习阶段的时候我们做的一个题目

可以看出这里我们只能在返回结果和销毁栈之中选一个进行操作,造成内存泄漏的问题

而c++在返回结果后则会自动销毁,这里析构函数的自动调用的优势就体现出来了

bool isValid(char * s){
    ST st;
    StackInit(&st);
    char*cur=s;
    while(*cur)
    {
        //左括号入栈
        if(*cur=='('||*cur=='['||*cur=='{')
        {
            StackPush(&st,*cur);
        }
        //右括号则将栈顶元素出栈与左括号匹配
        else
        {
            //如果出栈的时候栈为空,则不匹配
           if(StackEmpty(&st))
           {
               return false;
           }
           if( (*cur==')'&&StackTop(&st)!='(')||(*cur==']'&&StackTop(&st)!='[')||(*cur=='}'&&StackTop(&st)!='{') )
            {
                return false;
            }
            else
            {
                StackPop(&st);
            }
        }
        cur++;
    }
    //最后的时候栈要为空
    if(StackEmpty(&st))
    return true;
    else return false;
}

类的六个默认成员函数之拷贝构造函数

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
 

针对这种情况,拷贝构造函数顺势而生

拷贝构造函数特性

  • 构造函数的重载函数,只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰)
  • 未显示定义的时候系统自动生成,但不对内置值操作,对自定义类型自动调用
  • 传参的时候不能传值,否则就会无穷递归,导致程序崩溃
  • 参数引用的最好加上const,缩小权限,防止原数据被误改
  • 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。(重点!!!)关于这点可能干讲过于抽象,下面我们来详解一下

 我们先写一个栈类,栈类在我们类和对象的学习中可谓是经典范例

class Stack
{
public:
	Stack(size_t n = 4)
	{
		int* tmp = (int*)malloc(sizeof(int) * n);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}
		a = tmp;
		top = 0;
		capacity = n;
	}
	~Stack()
	{
		if (a)
		{
			free(a);
			top = capacity = 0;
		}
		
	}
	void Push(int x)
	{
		if (top == capacity)
		{
			int newcapacity = capacity == 0 ? 4 : 2 * capacity;
			int* tmp =(int*) realloc(a, newcapacity);
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			a = tmp;
			capacity = newcapacity;
		}
		a[top++] = x;
	}
private:
	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);

	Stack s2(s1);
	return 0;
}

 先创立了一个栈s1,插入四个数据后然后调用系统生成的拷贝构造函数生成对象s2

此时s2里的a和s1里的a是一样的,因为默认的拷贝构造函数就是值的拷贝

当程序结束的时候,按照栈后进先出的特性,我们先调用s2的析构函数,此时a所指向的空间第一次被销毁

到s1销毁的时候,调用s1的析构函数的时候,a所指向的空间面临第二次销毁,这个时候系统就会报错

但是如果没有向系统申请空间,就可以不用自己手写拷贝构造函数啦~

【C++】类和对象_第2张图片

拷贝函数的三大经典运用场景

  • 使用已存在对象创建新对象
  • 函数参数类型为类 类型对象
  • 函数返回值类型为类 类型对象

运算符重载

两个整形或者字符型数据可以用<,>,=号进行比较,但是两个类对象d1,d2就只能用函数比较了

但是函数名每个人的命名都不同,可能你写的函数名别人理解不了是什么意思,但是>,<这两个符号的意思在国际上是公认的,所以祖师爷就想能不能也用<,>两个符号比较两个类对象呢

运算符重载就这样诞生了

运算符重载的特性

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

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

函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现


const在类和对象中的应用

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
 

由前面所学我们可以知道this指针为隐藏指针,不能显示调用,那要怎么给他加const呢?

C++规定在函数后面加const修饰this指针,没啥原因记着就行

加上const就不能对类的任何成员作出修改,很符合我们只需要读的函数接口

没加上const就意味着成员可以修改,符合我们读或者写的接口

综上来说如果不需要对成员修改的函数接口我们都最好加上const,保证数据的安全稳定

class Date
{
public:
	Date(int year=2023,int month=8,int day=3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print1()
	{
		cout << " no const" << endl;
		cout << _year << _month << _day << endl;
	}

	void Print2()const
	{
		cout << " const" << endl;
		cout << _year << _month << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};
int main()
{
	const Date d1;
	Date d2;

	//d1.Print1();会报错
    d1.Print2();
	d2.Print2();//相当于权限的缩小
	return 0;
}

这里注意权限只可以缩小,不能放大,所以非const成员可以调用const成员函数和非const成员函数

再谈构造函数

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始
一次
,而构造函数体内可以多次赋值

class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};

这就引入我们的初始化列表,有了初始化列表,构造函数就完整了
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。

class Date
{
public:
    Date(int year, int month, int day)

    : _year(year)
    , _month(month)
    , _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

  • 1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  • 2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    • 引用成员变量——因为引用成员必须初始化
    • const成员变量——const成员必须初始化,且初始化后不能修改
    • 自定义类型成员(且该类没有默认构造函数时)
  • 3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  • 4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
  • 5.之前说的给声明变量缺省值,本质其实是给初始化列表里的值

隐式类型转换

【C++】类和对象_第3张图片

 都会先创建一个临时变量,临时变量具有常性,所以要加const

static成员

如果我们有计算程序中一个类在程序中创建了多少个对象的需求的时候,就需要一个在所有对象都共享的数据,这和之前的成员函数是不是非常相似

概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用
static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
 

特性:

1.静态成员为所有对象共享,存放在静态区

2.静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3.类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

非静态成员函数可以调用静态成员函数

静态成员函数不可以调用非静态成员函数——静态成员函数没有this指针,相当于函数没有参数,无法知道调用的是类里的非静态成员函数

友元

友元函数

友元函数是定义在类外的函数,但是它却可以访问类的私有成员

只要在类的任意位置,将函数的名字前加上一个friend即可

可以打破域的限制,但是会破坏耦合度,所以能不用就不用

  • 友元函数可访问类的所有成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同
     

友元类(简单介绍)

  • 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
  • 友元关系是单向的,不具有交换性。
  • 友元关系不能传递
  • 友元关系不能继承,在继承位置再给大家详细介绍

 A是B的友元类,那么在B中要在任意位置写个friend class A,那么A可以访问B的任意成员和函数

内部类(简单介绍)

就是在类里面再定义一个类,但是两个类并没有什么联系,是两个独立的类

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限

内部类就是外部类的友元类,可以访问外部类的所有成员,包括静态成员

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

总结

以上类和对象的大部分内容都讲的差不多了,掺杂本小白的个人理解,如有错误请指出!

类和对象是c++非常重要的一个部分,每个环节无不体现c++面向对象的思想

希望本篇对初入学习C++的大家有所帮助,也请大家多多点赞支持啦!!!

你可能感兴趣的:(c++,开发语言)