C语言是面向过程的,关注的是做一件事情的需要的步骤有哪些,通过一系列函数之间的调用配合来实现解决问题
例如打游戏,需要 拿出电脑,开机,联网,登录,进入游戏这几个步骤
C++是基于面向对象的,关注的是解决这一个问题参与的对象,依靠对象之间的交互来完成问题的解决
例如打游戏,对象就是电脑和人,对于电脑这个对象,内部可能有一些机理,比如联网,登录,对于人也有一些机理,例如肌肉运动之类的,这就类似于类中的函数
C语言中的结构体只能定义变量,但是在C++中,结构体中还可以定义函数,那么其实对于C++来说结构体也是一个类,而在C++中更常用class表示真正的类
class ClassName
{
void fun()
{
}
int a;
};//注意分号
class为定义类的关键字,ClassName是类的名字,{}是类的主体,之中是类的成员,包括成员变量和成员函数
class student
{
public:
void init()
{
cin>>_name>>_sex>>_age;
}
public:
char* _name;
char* _sex;
int _age;
}
class student
{
public:
void init();
public:
char* _name;
char* _sex;
int _age;
}
#include
void student::init()
{
cin>>_name>>_sex>>_age;
}
一般情况下尽量使用第二种,在类中定义的内部变量为了和临时变量能够区分清楚需要加入一定的前缀和后缀
C++实现封装的方式:用类将对象的属性和方法相结合,通过访问权限选择性的将接口提供给外部用户使用
注意
- public修饰的成员可以在类外被直接访问
- protected和private修饰的成员在类外不能被直接访问,两者类似
- 访问权限作用域从该访问限定符的位置开始直到下个访问限定符出现位置,或者到类的结束
- class的默认访问权限为private,struct的为public
这里主要讲解面向对象的三大特性(封装,继承,多态)之一,封装
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
说人话就是别问咋实现的,给你这东西你会用就行。
封装本质上是一种管理,让用户更方便使用类。在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类本身就是一个作用域,类中的所有成员都在类的作用域中,要在类外定义成员,例如变量或函数,时,需要使用作用域操作符指明属于哪个域
class student
{
public:
void init();
public:
char* _name;
char* _sex;
int _age;
}
void student::init()
{
cin>>_name>>_sex>>_age;
}
用类创建对象的过程,成为类的实例化,说人话就是用这个类创建了一个变量,例如 int a ,就是int实例化的一个过程。
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
char _a;
};
虽然看上去这个函数在类中,但实际上函数所占的空间并不在类中,因为如果是在类中的话,实例化相当数量的对象时,内存的占用会大大提高,为了避免这种事情的发生,会把成员变量放在一起,而成员函数放在公共代码区,这样即使实例化多个对象,成员函数始终只存在一个而且方便调用。所以类和对象的大小实际上跟结构体大小的计算方法时完全相同的
一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
这里为了引出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, d2;
d1.Init(2023,8,2);
d2.Init(2023,8,3);
d1.Print();
d2.Print();
return 0;
}
那么对于这个类Date,在函数中并没对不同对象进行区分,那么当d1调用Init函数和d2调用Init函数时,编译器如何对这两个对象进行区分呢
实际上在对象中有一个隐藏的this指针,当函数对对象进行操作的时候就可以通过这个指针找到对象的成员变量
如果类中什么都没有,就成为空类,会占一个字节的空间,来表示这里有一个类
但其实在空类中,编译器仍然会自动生成六个默认成员函数
我们之前写过一个Date类
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, d2;
d1.Init(2023,8,2);
d2.Init(2023,8,3);
d1.Print();
d2.Print();
return 0;
}
我们可以自己写一个公用的初始化函数Init(),但是,只要我们实例化一个类,就要进行一次调用,因此C++就会自动生成一个默认成员函数,名字与类名相同,称为默认构造函数,在每次实例化类的时候会自动进行调用,以保证每个数据成员都由一个合适的初始值
一般来说这个构造函数如何进行初始化是需要自己编写的,好在不需要自己进行调用,省了不少时间
下面是使用的演示,具体内容我们会在接下来的内容中讲到
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 _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1(2023, 8, 2);
Date d2(2023, 8, 3);
return 0;
}
我们可以在调试信息中看到d1,d2中成员变量的值
同理我们可以在输出窗口中加一些提示信息看到构造函数的调用情况
构造函数实际上是特殊的成员函数,而且构造函数本身的任务并不是创建变量而是初始化变量
同样的,与初始化相对应,还有变量的销毁,我们称之为析构函数,但要注意的是,销毁的其实是对象中的成员变量等,对象的销毁是由编译器自身完成的
用于拷贝和赋值自定义类型的值
拷贝构造函数:只有单个形参,该形参是对本自定义类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
后面两种情况实际上是因为在传值和返回值时会自动拷贝
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
比如对自定义类型对象的比较大小,加减,在写这类的函数时也应当考虑其实际含义是否有价值
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:
在许多情况下,我们并不想让调用的成员函数改变对应的成员变量,而在成员函数定义或声明时又无法显式写出this指针来限制,因此c++提供了这样一种语法来限制this指针所指向的对象不能被修改
class Date
{
public:
void print() const
{
cout<<_year<<' '<<_month<<' '<<_day<<endl;
}
private:
int _year;
int _month;
int _day;
};
可以理解为下面的第二种形式,其中的this所指向的内容不能被改变
void print() const;
void print(const Date* this);
在实际的操作中,const对象和非const对象都存在读和写的需求。而在只读的成员函数中则可以只写const成员函数,因为非const对象也可以调用const成员函数,这里实际上是权限的缩小,那么也可以得到如下结论,const对象不能调用非const成员函数,因为权限扩大了。const成员函数不能调用其他非const成员函数,非const成员函数可以调用其他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;
};
但实际上这并不算初始化,而属于初赋值,在实际的使用过程中,我们会遇到一些无法赋值的情况,例如const成员变量,引用变量,自定义类型成员且没有默认构造函数,由此c++发展出了初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
还是上面的日期类,我们可以这样初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
- 每个成员变量在初始化列表中只能出现一次
- 包含上面的特殊情况时必须放在初始化列表中初始化
- 尽量使用初始化列表进行初始化,因为自定义类型成员变量一定会先使用初始化列表初始化
- 初始化的顺序按照构造函数的声明顺序进行初始化
static的类成员被称为类的静态成员,包含静态成员变量和静态成员函数,此外在使用静态成员变量时一定要在类外进行初始化,后续将介绍静态成员变量和静态成员函数特性
- 静态成员属于整个类,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义(注意区分定义与声明的概念),定义不添加static,声明时添加
- 类的静态成员可以用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,因此也不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private的限制
注意: 区分static静态成员和const成员
友元是一种突破类的限制的方法,为了提供便利,破坏封装,提高耦合度,通常不建议封装,同样的,友元也分为友元函数和友元类
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字,也就是说这个函数可以直接访问类中的成员,十分方便
- 友元函数可以访问私有成员,但不是类的成员函数
- 友元函数不能用const修饰、
- 友元函数可以在类定义的任何地方声明,不受访问限定符的限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类的所有成员函数都是另一个类的友元函数,都可以直接访问私有成员
- 友元关系是单向的,不可以交换
- 友元关系是不可传递的
- 友元关系不能继承,后续介绍
概念: 类定义在另一个类的内部,则成为内部类,内部类是独立的一个类,不属于外部类,也不能通过外部类访问内部类的成员
注意:内部类是外部类的友元类,内部类可以通过外部类的对象参数访问外部类的所有成员,但外部类不是内部类的友元
特性:
匿名对象与c语言中的匿名结构体类似,可以直接在函数中使用,因为匿名对象不需要取名字,匿名对象和实名对象的基本操作都还在,比如构造与析构,只是没有名字,其次匿名对象的生命周期只有这一行。
class A()
{
public:
A(int a = 0)
:_a = a;
{
cout<<"A(int a = 0)"<<endl;
}
~A()
{
cout<<"~A()"<<endl;
}
int Fun(int input = x)
{
return _n + x;
}
private:
int _a;
static int _n;
}
int A::_n = 5;
int main()
{
A a1;//这里是实名对象,对象名为a1
A();//这里就是一个匿名对象
A().Fun(5);//如果想直接调用函数可以利用匿名对象
return 0;
}
在传参和返回值时,编译器会做一些优化,减少对象的拷贝,大多数编译器都会优化
例如要将返回的对象拷贝给一个新的对象,此时就会直接拷贝给新的对象,而不会利用临时对象再去拷贝,这样可以节省大量资源。