- C:各种各样的函数的定义, struct
- C++: 类: => 实体的抽象类型
- 实体: 属性, 行为 -》 ADT(abstract data type)
- 属性: 成员变量
- 行为: 成员方法
- 怎么理解OOP思想: 分析场景的实体,通过分析试图的属性和行为
- OOP语言的四大特征:
- 抽象
- 封装/隐藏
- 继承
- 多态
- 封装和隐藏通过访问限定符来体现
- public
- private
- protected
- 类是不占空间的,实例化成对象后才占空间,对象的内存在栈上, 对象的内存大小只和成员变量有关
- 属性一般是私有的,给外部提供公有的方法,来访问私有的属性
- 解释: 我会唱歌,你不能夺走我唱歌的能力,但是你能通过公有的访问我,让我唱一首歌
- 类体内实现的方法,自动转成inline内联函数
- 私有的成员变量: 商品的名字,价格,量
- 初始化商品的数据的构造函数
- 提供查询(get) 接受(set)外部私有变量的方法
- 类里面的私有变量写上_在前面
- 提供一个show()方法查询对象的信息,因为商品的属性只有在对象的时候才会访问,也才会占用栈的内存
- 用C风格的字符串定义的私有属性,其实是定义了一个char类型的数组,后面在类方法和构造函数里面,用指针指向一个字符串,然后用strcpy()把地址内存复制给他
- 在 C++ 中字符数组是不可修改的,但是字符指针可以直接指向字符串常量,在函数内部使用该字符指针是可以访问字符串常量的。
- 这个案例中,怎么看类的内存大小?
- char[20], double 8字节, int 4字节
- 对齐: 8是最大的, 20个char 补4个,24个字节
- int 4 字节,补4个字节 对齐
- 内存大小: 24 + 8 + 8 = 40 字节
#include <iostream>
#include <cstring>
using namespace std;
const int NAME_LEN = 20;
class CGoods
{
public: // 给外部提供公有的方法,来访问私有的属性
// 做商品数据初始化, char *
void init(const char *name, double price, int amount);
// 打印商品信息
void show();
// 给成员变量getXXX或者setXXX
// set函数用于接受外部的变量
void setName(const char *name) {strcpy(_name, name); }
void setPrice(double price) {_price = price; }
void setAmount(int amount) { _amount = amount; }
// get函数用于提供外部查询这个类的私有属性
const char* getName() { return _name; }
double getPrice() { return _price; }
int getAmout() { return _amount; }
private: // 属性一般是私有的
char _name[NAME_LEN];
double _price;
int _amount;
};
// 类外定义成员方法
void CGoods::init(const char *name, double price, int amount)
{
strcpy(_name, name); // 把指针指向的地址内存复制给_name
_price = price;
_amount = amount;
}
void CGoods::show()
{
cout << "name: " << _name << endl;
cout << "price: " << _price << endl;
cout << "amount: " << _amount << endl;
}
int main()
{
CGoods good; // 实例化一个对象
good.init("面包", 10.0, 200);
good.setPrice(20.0);
// good.setName("奶油面包");
good.show();
CGoods good2;
good2.init("商品2", 20.0, 999);
good2.show();
return 0;
}
- CGoods可以定义无数对象,每一个对象都有自己的成员变量,他们共享一套成员方法,放在代码段上的, 用this指针区分对象
- this指针就是用来区分方法对应的是哪个对象
- 类的成员方法一经编译,所有方法参数,都会加一个this指针,接受调用该方法的对象地址,CGoods *this
- 例如上一个CGoods的初始化为 init(const char *name, double price, int amount)
- 实际上是init(&goods1, const char *name, double price, int amount)
- 传入一个引用,才知道调用的是哪一个对象的地址
- 释放了内存,但是指针还在,要把指针变成空指针
- 用top, 栈顶元素的位置来判断空,满,入栈升高,出栈降低
- 压栈把第二个元素往下压,所以_pstack[0] _pstack[1]依次放元素
- 栈顶是-1的时候就视为空
- 动态扩容的时候不用mmcpoy,后面讨论深拷贝会讲
#include <iostream>
using namespace std;
class SeqStack
{
public:
void init(int size = 10)
{
_pstack = new int[size];
_top = -1;
_size = size;
}
void release()
{
delete []_pstack; // 释放了指向内存的地址,但是指针还在
_pstack = nullptr; // 把这个指针变成空指针
}
// 压栈
void push(int val)
{
if (full())
{
resize();
}
_top++;
_pstack[_top] = val; // 初始化: _pstack[0]放val
// 第二个入栈: _pstack[1]
// _pstack[++_top];
}
// 出栈
void pop()
{
if (empty())
{
return;
}
--_top;
}
// 获取栈顶元素
int top()
{
return _pstack[_top];
}
// 判断是否为空
bool empty() { return _top == -1; }
// 判断是否满了
bool full() { return _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];
}
delete []_pstack; // 释放内存
_pstack = ptmp; // 指向内存的指针指向新的地方
_size *= 2;
}
};
int main()
{
SeqStack s;
s.init(5);
for (int i = 0; i < 15; i++)
{
s.push(rand() % 100);
}
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
s.release();
return 0;
}
- 函数的名字和类名一样
- 没有返回值
- 构造函数,开辟资源,析构函数,释放资源
- 在return 0; 这一行执行析构函数
- 下面是对照,实例化后可以看到函数开始前调用构造函数,结束调用析构函数
- 实例化对象: 1. 在栈上开辟内存,2. 调用构造函数
- 运行下面代码,先构造的后析构,后构造的先析构
- 析构函数只能有一个,构造函数可以有多个,叫构造函数的重载
- 可以自己调用析构函数,掉用完后对象不存在,但是内存还在!再调用就是堆内存的非法访问了
#include <iostream>
using namespace std;
class SeqStack
{
public:
SeqStack(int size = 10)
{
cout << this << "SeqStack()" << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}
~SeqStack()
{
cout << this << "~SeqStack()" << endl;
delete []_pstack; // 释放了指向内存的地址,但是指针还在
_pstack = nullptr; // 把这个指针变成空指针
}
// 压栈
void push(int val)
{
if (full())
{
resize();
}
_top++;
_pstack[_top] = val; // 初始化: _pstack[0]放val
// 第二个入栈: _pstack[1]
// _pstack[++_top];
}
// 出栈
void pop()
{
if (empty())
{
return;
}
--_top;
}
// 获取栈顶元素
int top()
{
return _pstack[_top];
}
// 判断是否为空
bool empty() { return _top == -1; }
// 判断是否满了
bool full() { return _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];
}
delete []_pstack; // 释放内存
_pstack = ptmp; // 指向内存的指针指向新的地方
_size *= 2;
}
};
int main()
{
SeqStack s;
SeqStack s1;
for (int i = 0; i < 15; i++)
{
s.push(rand() % 100);
}
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
return 0;
}
- 没有提供任何构造函数的时候,回味我们生成默认构造函数和默认析构函数,是函数
- 还是以刚刚的顺序列为例子,看一个拷贝构造函数
int main()
{
SeqStack s;
SeqStack s1;
SeqStack s2(s1); // 拷贝构造函数
return 0;
}
- 上面的案例会报错,因为拷贝构造函数的时候,会把成员变量(私有属性)也拷贝过来,而int *_pstack;指向一个堆内存,两边都指向一个堆内存,s2调用析构函数释放堆内存,s1的指针依然存在,就变成了野指针。下图紫色区域为堆内存,黑色为栈内存,也可以看出来这个指针指向的不是类的内存
- 对象的成员变量有指针指向对象外部的资源容易出问题
- 对象浅拷贝有问题,要自定义拷贝构造函数
// 自定义拷贝构造函数
SeqStack(const SeqStack &src)
{
cout << "自定义拷贝函数" << endl;
// _pstack = src._pstack; // 这个是浅拷贝
_pstack = new int[src._size];
for (int i = 0; i < src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
- 假设数组里面放着对象,里面还有指针,那就会出问题, 用memcopy又变成浅拷贝了
int main()
{
SeqStack s;
SeqStack s1(10);
SeqStack s2(s1);
s2 = s1; // 赋值操作: 需要默认赋值构造函数
return 0;
}
- 又变成浅拷贝了
- 本来s2指向独属于自己的外部的资源,赋值让s2里面的指针指向s1的独属堆内存
- 这样更麻烦,s2丢掉了自己的独属的堆内存,连释放都做不到了所以需要一个赋值重载函数, 这个就是加一个释放当前内存的
- 如图所示
// 赋值重载函数
void operator=(const SeqStack &src)
{
cout << "赋值重载函数" << endl;
delete []_pstack;
// _pstack = src._pstack; // 这个是浅拷贝
_pstack = new int[src._size];
for (int i = 0; i < src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
#include <iostream>
using namespace std;
class SeqStack
{
public:
// 构造函数
SeqStack(int size = 10)
{
cout << this << "SeqStack()" << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}
// 自定义拷贝构造函数
SeqStack(const SeqStack &src)
{
cout << "自定义拷贝函数" << endl;
// _pstack = src._pstack; // 这个是浅拷贝
_pstack = new int[src._size];
for (int i = 0; i < src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
// 赋值重载函数
void operator=(const SeqStack &src)
{
cout << "赋值重载函数" << endl;
delete []_pstack;
// _pstack = src._pstack; // 这个是浅拷贝
_pstack = new int[src._size];
for (int i = 0; i < src._top; ++i)
{
_pstack[i] = src._pstack[i];
}
_top = src._top;
_size = src._size;
}
~SeqStack()
{
cout << this << "~SeqStack()" << endl;
delete []_pstack; // 释放了指向内存的地址,但是指针还在
_pstack = nullptr; // 把这个指针变成空指针
}
// 压栈
void push(int val)
{
if (full())
{
resize();
}
_top++;
_pstack[_top] = val; // 初始化: _pstack[0]放val
// 第二个入栈: _pstack[1]
// _pstack[++_top];
}
// 出栈
void pop()
{
if (empty())
{
return;
}
--_top;
}
// 获取栈顶元素
int top()
{
return _pstack[_top];
}
// 判断是否为空
bool empty() { return _top == -1; }
// 判断是否满了
bool full() { return _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];
}
delete []_pstack; // 释放内存
_pstack = ptmp; // 指向内存的指针指向新的地方
_size *= 2;
}
};
int main()
{
SeqStack s;
SeqStack s1(10);
SeqStack s2(s1);
s2 = s1; // 赋值操作: 需要默认赋值构造函数
return 0;
}
- 拷贝构造函数指向不同,但是内存相同的值相同
- 赋值重载函数指针指向相同,内存的值也相同
- C语言字符串是以\0结束的,所以strcpy 要 + 1
- 拷贝构造函数两步走: 1. 开辟新内存,2. 把内存的值拷贝过去
- 赋值重载函数: 1. 防止自赋值 2. 释放当前内存, 拷贝过去
- 赋值重载函数为什么不用void而是返回地址,因为可以str1 = str2 = str3;为了支持连续的赋值操作
#include <iostream>
#include <cstring>
using namespace std;
class Strings
{
public:
Strings(const char *str = nullptr) // 普通构造函数
{
if (str != nullptr)
{
m_data = new char[strlen(str) + 1]; // C语言字符串是以\0结束的
strcpy(this->m_data, str);
}
else // str = nullptr
{
m_data = new char[1];
*m_data = '\0';
}
}
// 拷贝构造函数: 实例化的对象外的堆内存地址相同
// 重新开一块内存,把other的值写进去
Strings(const Strings &other)
{
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
~Strings()
{
delete []m_data;
m_data = nullptr;
}
// 赋值重载函数
Strings& operator=(const Strings &other)
{
// 防止自我赋值
if (this == &other)
{
return *this;
}
// 释放原先内存
delete []m_data;
m_data = new char[strlen(other.m_data) + 1];
// 把右边的内存拷贝过来
strcpy(m_data, other.m_data);
return *this;
}
private:
char *m_data;
};
int main()
{
// 默认构造函数
Strings str1; // 默认参数,空指针
Strings str2("hello");
Strings str3 = "hello";
// 调用拷贝构造函数
Strings str4 = str3;
Strings str5(str3);
// 调用赋值重载函数
str1 = str2;
return 0;
}
-
自定义了构造函数后,编译器就不产生默认构造函数了
-
先执行构造函数的初始化列表再执行当前类类型构造函数体
-
在初始化列表写是int _amount = amount;
-
在构造函数体写是 int _amount; _amount = amount; 当然两者还有时间差
-
总结: 可以指定当前成员变量的初始化方法
#include <iostream>
#include <cstring>
using namespace std;
#define NAME_LENGTH 20
class CDate
{
public:
CDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class CGoods
{
public:
CGoods(const char *name, double price, int amount, int year, int month, int day)
: _date(year, month, day), // CDate _date;
_amount(amount), _price(price)
{
// strcpy函数的第二个参数需要传入一个字符数组的首地址
// 所以这里不用写*name
strcpy(_name, name);
}
void show()
{
cout << "name: " << _name << endl;
cout << "price: " << _price << endl;
cout << "amount: " << _amount << endl;
_date.show();
}
private:
char _name[NAME_LENGTH];
double _price;
int _amount;
CDate _date; // 成员对象 1.分配内存 2.调用构造函数
};
int main()
{
CGoods good1("ASD", 200, 100, 10, 10, 10);
good1.show();
return 0;
}
- ma输出是无效值,因为ma先定义先初始化,而这个时候的mb是无效值,先定义mb, 那么ma, mb都是10
#include <cstring>
using namespace std;
class Test
{
public:
Test(int data = 10): mb(data), ma(mb){ }
void show() { cout << "ma" << ma << " mb" << mb << endl; }
private:
int ma;
int mb;
};
int main()
{
Test t;
t.show();
return 0;
}
- 属于类的作用域
- 调用该方法时,需要依赖一个对象
- 可以任意访问对象的私有成员变量
- 会自动生成一个this指针,可以用这个指针访问私有形参
- static变量一定要在类外进行定义并且初始化
- 也是类的作用域
- 静态成员变量没有依赖的对象
- 每次产生新对象都会调用构造函数,所以静态变量的操作写在里面可以做记录
- 静态成员方法可以用对象来调用,但是更好是拿类来调用
- 静态方法不产生this指针
- 在静态方法不能访问普通的成员变量因为没有对象this指针
- 类的作用域
- 调用依赖对象,普通对象常对象都可以
- 常方法只能读不能写
- 常对象生成的是const CGoods *this指针
- 如果只读不写的函数尽量都写成常方法
#include <iostream>
#include <cstring>
using namespace std;
#define NAME_LENGTH 20
class CDate
{
public:
CDate(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void show()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class CGoods
{
public:
CGoods(const char *name, double price, int amount, int year, int month, int day)
: _date(year, month, day), // CDate _date;
_amount(amount), _price(price)
{
// strcpy函数的第二个参数需要传入一个字符数组的首地址
// 所以这里不用写*name
strcpy(_name, name);
_count++;
}
static void showcount() // 静态方法: 打印所有商品共享的信息
{
cout << "we have: " << _count << " goods" << endl;
// cout << _name; 会报错
}
void show() // 生成CGoods *this
{
cout << "name: " << _name << endl;
cout << "price: " << _price << endl;
cout << "amount: " << _amount << endl;
_date.show();
}
void show1()const // 生成const CGoods *this
{
cout << "name: " << _name << endl;
cout << "price: " << _price << endl;
cout << "amount: " << _amount << endl;
// _name++; // 会报错,只读不写
}
private:
char _name[NAME_LENGTH];
double _price;
int _amount;
CDate _date; // 成员对象 1.分配内存 2.调用构造函数
static int _count; // 静态成员变量
};
int CGoods::_count = 0;
int main()
{
CGoods good1("ASD", 200, 100, 10, 10, 10);
good1.show();
good1.showcount();
CGoods::showcount();
CGoods good2("ASD", 200, 100, 10, 10, 10);
CGoods::showcount();
CGoods::showcount();
// 常推对象
const CGoods good3("ASDDDD", 200, 100, 10, 10, 10);
// good3.show(); 常对象生成的是const CGoods *this
good3.show1();
return 0;
}
- 类的作用域指针,前面加类的作用域,调用的时候写依赖的对象
- 访问在堆上的对象要删除在最后,访问用->
- 静态变量不用写依赖的对象
- 指向成员方法的指针
#include <iostream>
using namespace std;
class Test
{
public:
void func() { cout << "call Test::func" << endl; }
static void static_func() { cout << "call static func" << endl; }
int ma;
static int mb; // 静态成员变量
};
int main()
{
Test t1;
Test *t2 = new Test(); // 在堆上的对象
// 类的作用域指针
// int *p = &Test::ma;
int Test::*p = &Test::ma;
t1.*p = 20; // 要把指针的作用域写上
cout << t1.*p << endl;
t2->*p = 30;
cout << t2->*p << endl;
// 定义指向成员方法的指针
void (Test::*pfunc)() = &Test::func;
(t1.*pfunc)();
(t2->*pfunc)();
delete t2;
return 0;
}