面向对象部分
类和对象
- 怎么理解OOP(面向对象)的思想呢?
看上面这幅图,当我们要采用OOP思想去解决实际问题时,首先我们会去分析问题中实体的属性和行为,然后将其抽象为一个数据类型,即ADT(实体的抽象数据类型),我们就可以依据此去编写所谓的类,实体的属性对应类的成员变量,而实体的行为对应类的成员方法,最后通过对类进行实例化后得到对象,这个对象就对应着实际问题当中的一个实体。(比如商品类—>可乐(对象))
- 如何去定义一个类呢?(class、struct)
- OOP的四大特征:
- 抽象
- 封装/隐藏
- 继承
- 多态
来看一个商品类CGoods的例子
const int NAME_LEN = 20;
//商品的抽象数据类型
class CGoods //第一个C代表class
{
public: //给外部提供公有的方法,来访问私有的属性
//做商品数据初始化
void init(/*CGoods* this, */ const char* name, double price, int amount)
{
strcpy(this->_name, name);
this->_price = price;
this->_amount = amount;
}
//打印商品信息
void show(/*CGoods* this, */)
{
cout << "name: " << _name << endl;
cout << "price: " << _price << endl;
cout << "amount: " << _amount << endl;
}
//给成员变量提供一个getXXX或setXXX的方法,类体内实现的方法会自动处理成inline内联函数
void setName(/*CGoods* this, */char* name) { strcpy(_name, name); }
void setPrice(/*CGoods* this, */double price) { _price = price; }
void setAmount(/*CGoods* this, */int amount) { _amount = amount; }
//const修饰指针的指向
const char* getName(/*CGoods* this, */) { return _name; }
double getPrice(/*CGoods* this, */) { return _price; }
int getAmount(/*CGoods* this, */) { return _amount; }
private://属性一般是私有的 total size=40 按8字节对齐
char _name[NAME_LEN]; //20+4
double _price; //8
int _amount; //4+4
};
// 如果在类外定义成员方法
/*
void CGoods::init(char* name, double price, int amount)
{
}
inline void CGoods::init(char* name, double price, int amount)
{
}
*/
int main()
{
CGoods good;
//新的vs编译器中,常量字符串不允许用普通指针来接收,用常指针
good.init("面包", 50.0, 100);
good.show();
good.setPrice(20.0);
good.setAmount(500);
good.show();
CGoods good2;
good2.init("空调", 10000.0, 20000);
good2.show();
return 0;
}
-
定义的类是不占空间的,类实例化的对象占内存空间
-
关于成员方法的定义方式:
-
在类的内部定义
在内部定义的成员方法是inline函数
-
在类的外部定义
需要在成员方法名前加上 类名作用域 类名::
类外定义的方法是一个普通函数,如果想要将它设置为内联,需要在返回值类型前加上inline关键字
-
-
对象的内存大小如何计算?
对象内存大小只与其成员变量有关,与成员方法无关,所以计算方式和计算结构体类型变量的方法是一样的
在VS下,我们也可以使用特定命令来查看:
cd 当前项目目录下
cl 面向对象.cpp /d1reportSingleClassLayoutCGoods
(注意d后面是一,不是l)
CGoods类可以定义无数对象,每个对象共享一套成员方法,但是它们都有自己的成员变量。但是这就会涉及到一个问题:既然对象共享一套成员方法,那么这一套成员方法到底是怎么去区分对象的呢?
实际上,程序一经编译,编译器都会为成员方法形参列表加上一个this指针,该this指针用来接收调用该成员方法的对象的地址
....
// 编译时为成员方法添加一个this指针
void init(/*CGoods* this, */ const char* name, double price, int amount)
{
strcpy(this->_name, name);
this->_price = price;
this->_amount = amount;
}
....
// 编译时对象调用成员方法的实际过程
int main()
{
CGoods good;
// good.init(&good,"面包", 50.0, 100);
good.init("面包", 50.0, 100);
return 0;
}
构造函数和析构函数
构造函数:
定义对象时,自动调用的,带参数,可以进行重载;
析构函数
不带参数,不能重载,只有一个析构函数;
举个例子:OOP实现一个顺序栈
class SeqStack
{
public:
SeqStack(int size=10) //构造函数可以带参数,因此可以提供多个构造函数,构造函数的重载
{
cout << "SeqStack(int size=10) " << endl;
//_pstack指向外部堆上的一块内存
_pstack = new int[size];//堆内存的开辟
_top = -1;
_size = size;
}
~SeqStack()//不带参数的,所以析构函数只有一个
{
cout << "~SeqStack()" << endl;
delete[] _pstack;
_pstack = nullptr;
}
void push(int val)
{
if (full())
resize();
_pstack[++_top] = val;
}
void pop()
{
if (empty())
return;
_top--;
}
int top()
{
return _pstack[_top];
}
bool empty()
{
return _top == -1;
}
bool full()
{
return _top == _size - 1;
}
private:
int* _pstack; // 动态开辟数组,存储顺序栈的元素
int _top; // 指向栈顶元素的位置
int _size; // 数组扩容的总大小
void resize()
{
int* ptmp = new int[_size * 2];
for (int i=0;i<_size;i++)
{
ptmp[i] = _pstack[i];
} // 为什么不用memcpy(ptmp, _pstack, sizeof(int)*_size);或者realloc?
/*
memcpy指向的是内存拷贝,如果数组里面存的是对象,而且对象的成员变量还指向外部资源,那么拷贝的对象的成员变量和原对象指向的就是同一块内存
*/
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
- 实例化一个对象,需要先开辟内存,然后再调用构造函数对成员变量进行初始化,构造函数调用完对象就产生了。
- 在执行return,准备出函数作用域的时候,在函数内部定义的对象会逐个调用析构函数,然后释放内存
- 调用完析构函数后,就认为对象不存在了,但是如果在函数内部调用了析构函数后,还没有出函数,那么对于在栈上构造的对象,其内存空间还在,函数结束,栈帧回收,其内存空间才会被释放
- 先构造的后析构,后构造的先析构。
不同内存区域的对象的构造和析构
// 栈上的对象(.stack),定义的时候构造,出函数作用域时析构
// 全局的对象(.data),定义的时候构造,程序结束时析构
// 堆上的对象,new的时候构造,一定要调用delete手动释放
SeqStack* ps = new SeqStack(60); // 相当于malloc内存开辟+SeqStack(60)
cout << ps->top() << endl;
delete ps;//delete和free区别
/* 这里delete 有两个操作
(1).先调用ps->~SeqStack()
(2). 然后free(ps);
*/
对象的深拷贝和浅拷贝
拷贝构造:使用已构造的对象来构造新的对象,编译器默认提供
赋值拷贝:一个对象赋值给另一个对象,编译器默认提供
需要根据实际情况判断使用浅拷贝是否有问题,如果有问题则需要自定义拷贝构造和赋值重载函数进行深拷贝
拷贝构造函数
int main()
{
SeqStack s; // 没有提供任何构造函数的时候,会自动生成默认构造函数
SeqStack s1(10);
//都是调用了拷贝构造,默认拷贝构造-->做直接内存拷贝
SeqStack s2 = s1;
SeqStack s3(s1);
// s2.operator=(s1)
// void operator=(const SeqStack& src)
// 默认的赋值函数 => 做直接的内存拷贝
s2 = s1; // 赋值操作 ,当对象浅拷贝有问题时,也需要定义赋值运算符重载
return 0;
}
- 编译器默认产生的拷贝构造执行的是内存拷贝,即浅拷贝,把s1的内存直接赋给s2,因为s1的成员变量中有指向堆内存的指针,所以经过内存拷贝构造的s2和s1指向堆上的同一块内存,那么当s1调用析构把堆内存释放后,s2指向的堆内存就不存在了,就会造成内存的非法访问。
-
按1所说,那浅拷贝一定都有问题吗?当然不是,上面的情况是对象有指向外部资源的成员变量,这种情况下使用浅拷贝就会出现问题
-
当出现对象成员变量访问外部资源时候,浅拷贝出现问题,我们就需要自定义拷贝构造函数,进行深拷贝
class SeqStack
{
public:
.....
.....
// 自定义拷贝构造 <== 对象的浅拷贝现在有问题了
SeqStack(const SeqStack& src)
{
cout << "SeqStack(const SeqStack& src)" << endl;
_pstack = new int[src._size];
for (int i=0;i<src._size;i++)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
.....
.....
}
赋值重载函数
赋值重载函数的实现:
- 防止自赋值
- 释放当前对象占用的外部资源
- 拷贝操作
int main()
{
SeqStack s; // 没有提供任何构造函数的时候,会自动生成默认构造函数
SeqStack s1(10);
SeqStack s2 = s1; // 调用了拷贝构造,默认拷贝构造-->做直接内存拷贝
SeqStack s3(s1); // 调用了拷贝构造,默认拷贝构造-->做直接内存拷贝
// void operator=(const SeqStack& src)
// 默认的赋值函数 => 做直接的内存拷贝
s2 = s1; // s2.operator=(s1)
return 0;
}
- 编译器默认提供赋值重载函数,做的是内存拷贝,当对象成员变量访问外部资源时,如果直接进行赋值拷贝,那么对象原来所指向的内存就丢掉了,无法进行释放
- 为避免1的情况,需要自定义赋值重载函数,需要先释放掉原来指向的外部内存空间
class SeqStack
{
public:
.....
.....
//赋值重载函数
void operator=(const SeqStack& src)
{
cout << "void operator=(const SeqStack& src)" << endl;
// 防止自赋值
if (this == &src)
return;
// 需要先释放当前对象占用的外部资源
delete[]_pstack;
// 深拷贝
_pstack = new int[src._size];
for (int i=0;i<src._size;i++)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
.....
.....
}
类和对象代码应用实践(后续补上)
自定义String类
循环队列
构造函数的初始化列表(*)
作用:指定当前类类型成员变量的初始化方式
// 日期类
class CDate
{
public:
CDate(int year, int month, int day) // 自定义了一个构造函数,编译器就不会再产生默认构造
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
void show() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
// 商品类
// 构造函数的初始化列表先执行,然后执行当前类类型的构造函数体
class CGoods
{
public:
CGoods(const char* name, int amount, int price, int year, int month, int day)
// 构造函数的初始化列表
:_date(year, month, day) // 相当于 CDate _date(year,month,day)
, _amount(amount) // 相当于int _amount = amount
, _price(price)
{
// 当前类类型构造函数体
strcpy(_name, name);
/*_amount = amount; //相当于int _amount; _amount = amount;
_price = price;*/
_count++;
}
private:
char _name[20];
int _amount;
double _price;
CDate _date; // 成员对象
static int _count; // 不属于对象,而是属于类级别的
};
-
定义了一个CDate类,并把该类的一个对象_date作为CGoods类的一个成员变量,又称作成员对象,当构造CGoods类的一个对象时,需要先为成员变量开辟内存,然后再调用构造函数,也就意味着当构造CGoods类的一个对象时,__date已先构造完成,在CGoods类的定义中如果没有显式调用Date类的构造函数,那么编译器就会调用其默认构造函数,但是我们在编写CDate类的时候,已经自定义了构造函数,编译器就不会生成默认构造函数,所以程序编译时会报错!!
-
那么,我们在编写CGoods类的时候,该在哪里调用CDate类的构造函数来构造__date对象呢?在CGoods类构造函数的初始化列表处
-
构造函数的初始化列表先执行,然后才会去执行构造函数体
// 在初始化列表中执行该语句,相当于执行int _amount = amount
_amount(amount)
// 在当前类类型的构造函数体中执行下面语句,相当于int _amount; _amount = amount; 也就是相当于已经构造好了,然后再执行赋值操作
_amount = amount;
// 对于基本数据类型其实问题不大,但是对于自定义类类型,如果在当前类类型的构造函数体执行如下语句
_date = CDate(y,m,d) // 实际上是在用一个对象给另一个已构造好的对象赋值
// 只有在初始化列表中,才能调用自定义的构造函数构造成员对象
_date(y,m,d)
- 还有一个需要注意的地方:
初始化列表可以指定成员变量的初始化方式,但要注意在初始化列表中成员变量的初始化顺序和它们的定义顺序有关,这里ma先定义,所以实际在初始化时先执行int ma = mb,然后执行int mb=data
类的各种成员变量/成员方法及其区别(*)
静态成员变量:属于类级别,在类中声明,且一定要在类外进行定义并且进行初始化
静态成员方法:用来访问所有对象共享的信息(静态成员变量。。),外部调用的时候使用类名作用域来调用—类名::方法名(…),它和普通成员方法本质的区别是:编译的时候不会给该方法的参数列表添加this指针,也因此静态成员方法是无法访问普通成员变量。
class CGoods
{
...
...
// 静态成员方法是没有this指针的
static void showCGoodsCount()
{
cout << "所有商品的种类数量是:" << _count << endl;
}
private:
char _name[20];
int _amount;
double _price;
CDate _date; // 成员对象
static int _count; // 静态成员变量
};
// static成员变量一定要在类外进行定义
int CGoods:: _count = 0;
int main()
{
....
CGoods::showCGoodsCount(); // 调用类的静态成员方法
....
return 0;
}
常成员方法
常对象无法调用普通的成员方法,因为const CGoods* 无法传给 CGoods* this,而对于常成员方法来说,在编译时会给参数列表添加const CGoods* this指针,但是在常成员方法中也无法调用普通的成员方法
- 常成员方法中不能修改成员变量的值,因为生成的是const this指针
class CGoods
{
...
// 常成员方法
void show()const // 生成const CGoods* this
{//this的指向都被修饰成const,所以都不能被修改
cout << "name: " << _name << endl;
cout << "amount: " << _amount << endl;
cout << "price: " << _price << endl;
_date.show();//编译器把this指向的对象都看作是常对象,所以这里show也得是一个const成员方法
}
...
private:
char _name[20];
int _amount;
double _price;
CDate _date; // 成员对象
};
总结:
3种成员方法(核心在于this指针):
普通的成员方法--------------->编译器产生this指针
1.属于类的作用域
2.调用该方法时,需要依赖一个对象(常对象无法调用普通成员方法)
3.可以任意访问对象的私有成员变量static静态成员方法:------------>没有this指针
1.属于类的作用域
2.用类名作用域来调用方法
3.可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其他的static静态成员)const 常成员方法---------------->产生const* this指针
1.属于类的作用域
2.调用依赖一个对象,普通对象或者常对象都可以调用常成员方法
3.可以任意访问对象的私有成员,但是只能读,不能写建议:
只要是只读操作的成员方法,一律实现成const常成员方法