类和对象
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。封装后的类隐藏了它的实现细节,用户只能使用接口而无法访问实现部分。
C++面向对象的三大特性:封装,继承,多态
具有相同性质的对象,我们可以抽象为类。
定义抽象数据类型
封装的意义:
- 将属性和行为作为一个整体,来表现生活中的事物
- 将属性和行为加以权限控制
意义一:
语法:
class 类名{ 访问权限: 属性/行为};
示例1:设计一个圆类,求圆的周长:
const double PI = 3.14;
class Circle
{
public: //访问权限
int m_r; //属性:半径
double calculateZC() //行为(通常是函数):获取圆的周长
{
ruturn 2*PI*m_r;
}
};
int main()
{
Circle c1; //通过圆类创建具体的圆(对象)
c1.m_r = 10; //给圆对象的属性进行赋值
cout << "圆的周长为:" << c1.calculataZC() <<endl;
}
通过一个类创建一个对象的过程称之为实例化。
可以通过行为来赋值,示例二:
class Student
{
public:
string m_Name;
int m_Id;
void showStudent()
{
cout << "姓名:" << m_Name << "学号:" << m_Id << endl;
}
void setName(string name)
{
m_Name = name;
}
void setId(int id)
{
m_Id = id;
}
}
int main()
{
Student.setName("张三");
Student.setId(1);
Student.showStudent();
}
属性和行为我们称为成员,其中属性称为成员属性,成员变量。行为称为成员函数,成员方法。
意义二:
在设计类时,我们可以把属性和行为放在不同的权限下加以控制,访问权限有三种:
- public 公共权限 //成员类内可访问,类外可访问
- protected 保护权限 //成员类内可访问,类外不可访问
- private 私有权限 //成员类内可访问,类外不可访问
class person
{
public:
string m_Name;
protected:
int m_Password;
private:
string m_Website;
public:
void func()
{
m_Name = "Haru";
m_Password = 123456;
m_Website = "www.csdn.com";
}
}
在了解到这里我们发现,struct和class非常类似,事实上,在C++中,struct和class唯一的区别就是默认的访问权限不同:struct默认为公共,class默认为私有。
struct c1
{
int m_A; //默认为public
}
class c2
{
int m_B; //默认为private
}
成员属性私有化
优点:将所有成员属性设置为私有,可以自己控制读写权限;对于写权限,我们可以检测数据的有效性
示例:
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
void setName(string name)
{
m_Name = name;
}
string getName()
{
return m_Name;
}
int getAge()
{
return m_Age;
}
void setMoney(int money)
{
if(money > 10000000000 || money < 0){
cout << "辣你是真滴🐂🍺" <<endl;
return; //强制退出
}
m_money = money;
}
private:
string m_Name; //可读可写
int m_Age = 18; //只读
int m_money; //只写
};
int main()
{
Person c1;
c1.setName("Haru");
cout << "年龄为:" << c1.getAge() << endl;
}
例题:分别写一个全局函数和一个成员函数来判断两个信息是否时同一个人(该类内包含姓名,id,性别)
class human
{
public:
void setName(string name)
{
m_name = name;
}
void setId(int id)
{
m_id = id;
}
void setSex(string sex)
{
m_sex = sex;
}
string getName()
{
return m_name;
}
int getId()
{
return m_id;
}
string getSex()
{
return m_sex;
}
bool isSamebyclass(human &c2) //注意这里是类的引用,可以防止拷贝
{
if (m_id == c2.m_id && m_name == c2.m_name && m_sex == c2.m_sex) {
return true;
}else {
return false;
}
}
private:
string m_name;
int m_id;
string m_sex;
};
bool isSame(human& c1, human& c2)
{
if (c1.getName() == c2.getName() && c1.getId() == c2.getId() && c1.getSex() == c2.getSex()) {
return true;
}
else {
return false;
}
}
int main()
{
human c1, c2;
c1.setName("Haru");
c1.setId(233);
c1.setSex("B");
c2.setName("Haru");
c2.setId(233);
c2.setSex("G");
bool ret = isSame(c1, c2);
bool ret2 = c1.isSamebyclass(c2);
if (ret && ret2) {
cout << "这是同一个人" << endl;
}
else {
cout << "这不是同一个人" << endl;
}
return 0;
}
这道例题主要是提醒函数定义时参数写引用(这要成为一个习惯),以及调用函数时该怎么写。
实际上,我们应该把类的定义和函数部分写在不同的地方,类的定义写在头文件中,再在源文件中包含进来:
#include"human.h"
对象的初始化和清理
一个对象没有初始状态,会有一定的风险,同样使用完一个对象没有及时清理,也会造成一定的安全问题。
构造函数和析构函数
构造函数:主要作用于在创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在构建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
如果我们不提供构造和析构,编译器会提供,但编译器提供的构造函数和析构函数是空实现。
构造函数语法:类名(){}
- 构造函数没有返回值也不用写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时会自动调用构造,无需手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 没有返回值也不加void
- 函数名称是类名前面加 ~
- 析构函数没有参数,不可发生重载
- 程序在对象销毁前会自动析构,无需手动调用且只会调用一次
class Person
{
public:
Person(){} //编译器也会写这么一行(如果你不写)
~Person(){} //同上
}
构造函数的分类和调用
两种分类方式:
按参数分为:有参构造和无参构造(默认构造)
按类型分为:普通构造和拷贝构造
三种调用方式:
括号法
显示法
隐式转换法
假设Person类中有一个成员为age
Person(int a)
{
age = a; //有参构造可以进行的操作
}
初始值列表
Person(int a,int b,int c):age1(a),age2(b),age3(c+a){}
上面的写法均为普通构造,下面为拷贝构造的写法:
Person(const Person &p)
{
age = p.age;
}
括号法:
void test01()
{
person p1; //无参构造函数调用
Person p2(10); //括号法有参构造函数调用
Person p3(p2); //括号法拷贝构造函数调用
}
注意:默认构造时不要加(),因为这样编译器会认为是一个函数的声明,不会认为是在创建对象。
显示法:
Person p2 = Person(10); //有参构造
Person p3 = Person(p2); //拷贝构造
//Person(10) :匿名对象 特点:当执行结束后,系统会立即回收匿名对象
隐式转换法
Person p4 = 10; //等价于Person p4 = Person(10)
Person p5 = p4; //拷贝
c++中拷贝构造函数调用时机有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数传值
- 以值传递返回局部对象
Person(int a = 0, int b = 0, int c = 0){
age1 = a;age2 = b;age3 = c;}
Person p1; //三个成员值为0,0,0
Person p2(1,2); //三个成员值为1,2,0
析构函数
在对象生命周期结束时自动执行,完成清理内存的工作,并且可以执行指定的其他操作。比如对象中含有new,需要有delete的指定操作。
一个类只能有一个析构函数,不接受参数(所以不支持重载)
~类名(){
指定操作;}
this关键字
每个对象有自己独立的数据空间,但是类的成员函数只存储了一份,为所有对象共享。当通过对象调用非静态函数时,需要把调用对象的地址也传递给成员函数,以确定成员函数要处理的数据是哪一个对象的数据,成员函数通过this指针接受调用对象的地址,所以每一个成员函数(非静态)
都有一隐含的指针变量this。
例如,如果调用
P1.changeAge();
则编译器负责把P1的地址传递给changeAge的隐式形参this,可以等价地认为写成了如下地形式:
Person::changeAge(&P1);
在成员函数内部,我们可以直接调用该函数的对象的成员,而无需通过成员访问运算符,因为this所指的正是这个对象,任何对类成员的直接访问都被看作this的隐式引用,也就是说,当changeAge使用age时,它隐式地使用this指向地成员,即
this->age;
static成员
在类内数据成员的声明前加上关键字static,该数据就是类内的静态数据成员,静态数据成员也被称作是类的成员。
无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问;
对该类的多个对象来说,静态数据成员只分配一次内存;
静态数据成员的值对每个对象都是一样的,而非静态数据成员,每个类对象都有自己的拷贝。
关于静态数据成员的初始化:
数据类型 类类型名::静态数据成员名 = 值
int Person::age = 0;
关于静态数据成员的访问:
P1.age;
Person::age; //都是同一个变量空间
成员函数前面加static修饰成静态成员函数,静态成员函数声明时前面加static关键字,在类外写函数实现时不需要,静态成员函数是不依赖于对象成员的函数,通常用于定义一些工具函数。
普通成员函数一般都隐含一个this指针,静态成员函数由于不是与任何的对象相联系,因此它不具备this指针,静态成员函数可以访问静态数据成员和静态成员函数,但是不能访问非静态数据成员和非静态成员函数。非静态成员函数则可以任意访问静态成员函数和静态成员。
const成员和const对象
class Clock{
const int hour;
int minute,second;
public:
Clock(int h ,int m ,int s):hour(h),minute(m),second(s){}
}; //正确
Clock(int h ,int m ,int s):hour(h){minute = m;second = s} //正确
Clock(int h ,int m ,int s){hour = h; minute = m;second = s;} //错误
Clock(int m ,int s){minute = m;second = s;} //错误
声明常成员函数:
格式:类型 成员函数名(参数表)const{};
void showClock() const{cout<<hour<<minute<<second;}
在常成员函数中,不能改变数据,常成员函数不能调用非const成员函数,因为非const成员函数可能会改变数据,导致const函数间接改变了数据。
关于const函数重载
函数重载,其中一个为const函数是合法的重载。对于利用const重载的成员函数,非常对象默认调用非const成员函数,若没有非const成员函数再调用const成员函数。常对象则必须调用const成员函数。
定义const对象
类名 const 对象名(实参表列);
或者
const 类名 对象名(实参表列);
一个对象被声明为常对象,则不能调用该对象的非const型的成员函数(除了系统自动调用的隐式的构造函数和析构函数)
指向对象的指针
1.指向对象的指针:
Person p1(0,1,1); Person *p = &p1;
对象名p1可以访问任何公有成员,指针p可以访问任何公有成员,p的值是可以改变的
2.指向常对象的指针:
指向的对象可以是const也可以不是,但只能访问对象的const成员
Person p1(0,1,1); const Person *p = &p1;
对象名p1可以访问任何公有成员,指针p可以访问const公有成员,p的值是可以改变的
3.指向对象的常指针:
Person p1(0,1,1); Person const *p = &p1;
对象名p1可以访问任何公有成员,指针p可以访问任何公有成员,p的值是不可改变的
4.指向常对象的常指针:
const Person p1(0,1,1);const Person const *p = &p1;
对象名p1可以访问const公有成员,指针p可以访问const公有成员,p的值是不可改变的