一、类和对象、this指针
1、谈谈对面向对象(OO,Object Oriented)的理解
类(属性->成员变量;行为->成员方法) =》
对于我们C++面向对象编程而言,要写各种各样的类,即实体的抽象类型(ADT,Abstract data type)。比如我们要写校园人员管理系统或者公司的人员信息管理系统,我们在以OOP的思想解决这一问题的时候,我们首先要搞清楚这个问题场景里面有哪些实体,这些实体需要我们在代码上抽象出来一个类,计算机世界用一个类来代表我们现实世界的一个抽象类型。从实体的属性和行为得到ADT,得到这个抽象类型以后,我们就可以在计算机里面输出一个类,实体的属性对应类上的成员变量,实体的行为对应类上的成员方法。当我们用类去实例化对象的时候,对象是占内存空间的,就像我们现实世界中实体要占空间一样,对象在逻辑意义上才代表了我们现实世界中的实体。
OOP有四大特征:抽象、封装(隐藏)、继承和多态。
封装是通过类里面的访问限定符体现出来的,访问限定符有三种:public公有的、private私有的、protected保护的。
#include <iostream>
using namespace std;
//类 =》 商品
const int NAME_LEN = 20;
class CGoods//=>商品的抽象数据类型
{
public://给外部提供公有的成员方法,来访问私有的属性
//做商品数据的初始化用的
void init(const char* name, double price, int amount);
//打印商品信息
void show();
//给成员变量提供一个getXXX或setXXX的方法 ,类体内实现的方法,自动处理成inline内联函数
void setName(char* name) { strcpy(_name, name); }
void setPrice(double price) { _price = price; }
void setAmount(int amount) { _amount = amount; }
const char* getName() { return _name; }
double getPrice() { return _price; }
int getAmount() { return _amount; }
private://属性一般都是私有的成员变量
char _name[NAME_LEN];
double _price;
int _amount;
};
//在类外定义成员方法就是在函数名前面加类的作用域
//如果想让它实现成inline在最前面加个inline
void CGoods::init(const char* name, double price, int amount)
{
strcpy(_name, name);
_price = price;
_amount = amount;
}
void CGoods::show()
{
cout << "name:" << _name << endl;
cout << "price:" << _price << endl;
cout << "amount" << _amount << endl;
}
int main()
{
/*
CGoods可以定义无数的对象,每一个对象都有自己的成员变量
但是他们共享一套成员方法
*/
/*
show() => 怎么知道处理哪个对象信息?
init(name,price,amount) => 怎么知道把信息初始化给哪一个对象?
类的成员方法一经编译,所有的成员方法参数,都会加一个this指针,接受调用该方法的对象的地址。用this指针来区分调用该成员方法的不同对象。
*/
CGoods good1;//类实例化了一个对象
//面包是常量字符串,不允许普通指针来接收,所以init成员方法里的name被定义成了const char*
good1.init("面包", 10.0, 200);//init(&good1,"面包", 10.0, 200)
good1.show(); //show(&good1)
good1.setPrice(20.5);
good1.setAmount(100);
good1.show();
CGoods good2;//类实例化了一个对象
good2.init("空调", 10000.0, 50);
good2.show();
return 0;
}
运行结果
类体内实现的方法,自动处理成inline内联函数
对象占用内存的大小,只和成员变量有关(不考虑static成员变量),与成员方法无关
查看对象占用内存大小:打开vs工具->visual studio命令提示,cd test.cpp所在目录,输入
cl test.cpp /d1reportSingleClassLayoutCGoods
this指针的作用:
一个类产生了很多对象,每一个对象都有自己的成员变量,但是同一个类型的对象共享一套成员方法。那么一套成员方法如何区分不同的对象呢?就是通过this指针来区分的,成员方法一经编译,方法的参数都会添加一个this指针,用来接收调用该方法的对象的地址。所以在成员方法里访问的其他成员变量或调用其他的成员方法前面都会默认加this指向。
二、掌握构造函数和析构函数
构造函数:定义对象时,自动调用的;可以重载的;构造完后,对象产生了
析构函数:不带参数,不能重载,只有一个析构函数;析构完成后,对象就不存在,但内存还在。
#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();
_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会根据指定的大小进行扩容,但是涉及的是内存拷贝,内存拷贝在对象里面并不适合。
//在这块用memcpy没有问题,因为这里面数组栈里面存的都是整型,可以直接做内存的拷贝。
//但是有时在扩容的时候,有可能是对象会产生问题。
//memcpy(ptmp,_pstack,sizeof(int)*_size);
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
SeqStack gs;//全局对象,程序结束后析构
int main()
{
SeqStack* ps = new SeqStack(60);//malloc内存开辟+SeqStack对象构造
ps->push(70);
ps->push(80);
ps->pop();
cout << ps->top() << endl;
delete ps;//先调用ps->~SeqStack()+然后free(ps)
//1.开辟内存;2.调用构造函数
SeqStack s;
for (int i = 0; i < 15; i++)
{
s.push(rand() % 100);
}
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
SeqStack s1(50);
s1.~SeqStack()//析构函数调用后对象不存在了,就不要再调用它的方法了
s1.push(30);//堆内存的非法访问,因为上面已经调用析构函数了!
return 0;
}
.data
定义的对象在程序启动时构造,程序结束被析构
heap
上的对象new的时候构造,delete的时候析构
stack
定义的对象在进入函数进行到它定义的地方才构造,出函数就会被析构
三、对象的深拷贝和浅拷贝
拷贝构造:是在用已存在的对象构造新生成的对象的时候自动调用;
赋值函数:用已存在的对象给已存在的对象进行赋值的过程自动调用的函数。
对象默认的拷贝构造和赋值函数都是做内存的数据拷贝,也就是浅拷贝。关键是对象如果占用外部资源,那么浅拷贝就出现问题了!析构时会对同一资源进行多次释放,后析构的就会出错!
举例代码:
(1)栈
#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 << "SeqStack(const SeqStack& src)" << endl;
_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 << "operator=(const SeqStack& src)" << endl;
//防止自赋值
if (&src == this)
{
return;
}
//需要先释放当前对象占用的外部资源
delete[]_pstack;
_pstack = new int[src._size];
for (int i = 0; i <= src._top; i++)
{
_pstack[i] = src._pstack[i];
}//做深拷贝
//realloc和memcpy(_pstack,src._pstack,sizeof(int)*_size//做浅拷贝,不建议用
_top = src._top;
_size = src._size;
}
//析构函数
~SeqStack()
{
cout << this << "~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];
}
delete[]_pstack;
_pstack = ptmp;
_size *= 2;
}
};
int main()
{
SeqStack s;//没有提供任何构造函数的时候,会为你生成默认构造和默认析构
SeqStack s1(10);
SeqStack s2 = s1;//默认拷贝构造函数=》做直接内存数据拷贝
//SeqStack s2(s1);
s2 = s1;//默认的赋值函数=》做直接内存拷贝
return 0;
}
运行结果:
拷贝的时候为什么用for循环而不用memcpy?
因为在做数据拷贝的时候,假设数组里面放的都是整型,它就不占用整型之外的资源,就是这块内存只是放了一个值而已,那么可以用memcpy内存拷贝。但如果数组里面放的不是整型而是对象,每个对象都有指针,还指向了外部资源,如果用memcpy只是把对象本身的内存拷贝过来,做的是浅拷贝,这样拷贝完对象的指针和原对象指针指向的是同一块资源,在析构对象的时候就会出错。
(2)string类
#include <iostream>
using namespace std;
class String
{
public:
String(const char* str = nullptr)
{
if (str != nullptr)
{
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
else
{
m_data = new char[1];
*m_data = '\0';
}
}
String(const String& src)
{
m_data = new char[strlen(src.m_data) + 1];
strcpy(m_data, src.m_data);
}
~String()
{
delete[]m_data;
m_data = nullptr;
}
//调用String&是为了支持连续的operator=赋值操作
String& operator=(const String& src)
{
if (&src == this)
{
return *this;
}
delete[]m_data;
m_data = new char[strlen(src.m_data) + 1];
strcpy(m_data, src.m_data);
return *this;
}
private:
char* m_data;//用于保存字符串
};
int main()
{
//调用构造函数
String str1;
String str2("hello");
String str3 = "world";
//调用拷贝构造函数
String str4 = str3;
String str5(str3);
//调用赋值重载函数
str1 = str2;
return 0;
}
strlen(str)并不考虑"\0"的内存,因此新开辟空间时,要加上1。字符串构造时,如果传入为空,那给自己的还申请一个char空间,避免后续频繁地判空操作。
(3)循环队列
#include <iostream>
using namespace std;
class Queue
{
public:
Queue(int size = 10)
{
_pQue = new int[size];
_front = _rear = 0;
_size = size;
}
Queue(const Queue& src)
{
_front = src._front;
_rear = src._rear;
_size = src._size;
_pQue = new int[_size];
for (int i = _front; i != _rear; i = (i + 1) % _size)
{
_pQue[i] = src._pQue[i];
}
}
Queue& operator=(const Queue& src)
{
if (&src == this)
{
return *this;
}
delete[]_pQue;
_front = src._front;
_rear = src._rear;
_size = src._size;
_pQue = new int[_size];
for (int i = _front; i != _rear; i = (i + 1) % _size)
{
_pQue[i] = src._pQue[i];
}
return *this;
}
~Queue()
{
delete[]_pQue;
_pQue = nullptr;
}
void push(int val)//入队操作
{
if (full())
resize();
_pQue[_rear] = val;
_rear = (_rear + 1) % _size;
}
void pop()//出队操作
{
if (empty())
return;
_front = (_front + 1) % _size;
}
int front()
{
return _pQue[_front];
}
bool full() { return (_rear + 1) % _size == _front; }
bool empty() { return _front == _rear; }
private:
int* _pQue;//申请队列的数组空间
int _front;//指示队头的位置
int _rear;//指示队尾的位置
int _size;//队列扩容的总大小
void resize()
{
int* ptmp = new int[2 * _size];
int index = 0;
for (int i = _front; i != _rear; i = (i + 1) % _size)
{
ptmp[index] = _pQue[i];
index++;
}
delete[]_pQue;
_pQue = ptmp;
_front = 0;
_rear = index;
_size *= 2;
}
};
int main()
{
Queue queue;
for (int i = 0; i < 20; i++)
{
queue.push(rand() % 100);
}
while (!queue.empty())
{
cout << queue.front() << endl;
queue.pop();
}
cout << endl;
Queue queue1 = queue;
return 0;
}
四、构造函数的初始化列表
在构造函数的函数体里初始化和在初始化列表中初始化有什么区别?
比如_amount=a
在初始化列表中相当于int _amount=a;
,在构造函数函数体中相当于int _amount; amount=a;
在初始化列表中_date(y,m,d)
相当于CDate _date(y,m,d)
指定了日期对象的构造方式;但是在构造函数体需要这样构造对象_date=CDate(y,m,d)
,需要先把这个对象构造起来再赋值给_date,但是没有指定构造方式用的是默认构造,而它没有默认构造,所以成员对象的初始化必须写在当前构造函数的初始化列表。
#include <iostream>
using namespace std;
class CDate
{
public:
CDate(int y, int m, int d)//自定义了一个构造函数,编译器就不会再产生默认构造函数了
{
_year = y;
_month = m;
_day = d;
}
void show()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//构造函数的初始化列表:可以指定当前对象成员变量的初始化形式
//CDate信息是CGoods信息的一部分 a part of...组合关系
class CGoods
{
public:
//"CDate":没有合适的默认构造函数可用
CGoods(const char* n, int a, int p, int y, int m, int d)
//构造函数的初始化列表
:_date(y,m,d)
,_amount(a)//相当于int _amount = a
,_price(p)
{
//当前类类型构造函数体
strcpy(_name, n);
//_amount = a;相当于int _amount;_amount=a;
}
void show()
{
cout << "name:" << _name << endl;
cout << "amount:" << _amount << endl;
cout << "price:" << _price << endl;
_date.show();
}
private:
char _name[20];
int _amount;
double _price;
CDate _date;//成员对象 1.分配内存 2.调用构造函数
};
int main()
{
CGoods good("商品1", 100, 35.0, 2019, 5, 21);
good.show();
return 0;
}
成员变量的初始化和它们定义的顺序有关,和构造函数初始化列表中出现的先后顺序无关!
举例:
#include <iostream>
using namespace std;
class Test
{
public:
Test(int data = 10) :mb(data), ma(mb) {}
void show() { cout << "ma:" << ma << endl << "mb:" << mb << endl; }
private:
int ma;
int mb;
};
int main()
{
Test t;//0xcccccccc -858993460
t.show();
return 0;
}
运行结果:
五、类的各种成员方法及区别
类的各种成员方法 - 成员方法/变量
1.普通的成员方法=>编译器会添加一个this形参变量
(1)属于类的作用域
(2)调用该方法时,需要依赖一个对象(常对象无法调用)
(3)可以任意访问对象的私有成员
2.static静态成员方法=>不会生成this形参
(1)属于类的作用域
(2)用类名作用域来调用方法
(3)可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其他的static静态成员)
(4)static成员变量在类内属于声明,一定要在类外进行定义并且初始化
(5)如果方法访问的是所有对象共享的信息的话,最好把这个方法写成static方法
3.const常成员方法=>const CGoods *this
(1)属于类的作用域
(2)调用依赖于一个对象,普通对象或者常对象都可以
(3)可以任意访问对象的私有成员,但是只能读,而不能写
#include <iostream>
using namespace std;
#if 1
class CDate
{
public:
CDate(int y, int m, int d)
{
_year = y;
_month = m;
_day = d;
}
void show()const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class CGoods
{
public:
CGoods(const char* n, int a, int p, int y, int m, int d)
:_date(y,m,d)
,_amount(a)
,_price(p)
{
strcpy(_name, n);
_count++;//记录所有产生的新对象的数量
}
//普通成员方法
void show()//打印商品的私有信息CGoods *this
{
cout << "name:" << _name << endl;
cout << "amount:" << _amount << endl;
cout << "price:" << _price << endl;
_date.show();
}
//常成员方法 只要是只读操作的成员方法,一律实现成const常成员方法
void show()const//const CGoods *this
{
cout << "name:" << _name << endl;
cout << "amount:" << _amount << endl;
cout << "price:" << _price << endl;
_date.show();
}
//静态成员方法,没有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 good1("商品1", 100, 35.0, 2019, 5, 21);
good1.show();
CGoods good2("商品2", 100, 35.0, 2019, 5, 21);
good2.show();
CGoods good3("商品3", 100, 35.0, 2019, 5, 21);
good3.show();
CGoods good4("商品4", 100, 35.0, 2019, 5, 21);
good4.show();
//统计所有商品的总数量
CGoods::showCGoodsCount();
const CGoods good5("非卖品商品5",100,35.0,2019,5,21);
good5.show();
return 0;
}
#endif
运行结果:
六、指向类成员的指针
#include <iostream>
using namespace std;
class Test
{
public:
void func() { cout << "call Test::func" << endl; }
static void static_func() { cout << "Test::static_func" << endl; }
int ma;
static int mb;
};
int Test::mb;
int main()
{
Test t1;
Test* t2 = new Test();
//指向成员方法的指针
//如果写成void (*pfunc)() = &Test::func;会报错无法从"void(_thiscall Test::*)(void)"转换为"void (_cdecl*)(void)"
void (Test:: * pfunc)() = &Test::func;
(t1.*pfunc)();
(t2->*pfunc)();
//指向static成员方法的指针
void (*static_func)() = &Test::static_func;
Test::static_func();
//int *p = &Test::ma;会报错无法从int Test::*转化为int*,改成int Test::*p = &Test::ma;不会报错。
int Test::* p = &Test::ma;
t1.*p = 20;
cout << t1.*p << endl;
t2->*p = 30;
cout << t2->*p << endl;
int* p1 = &Test::mb;
*p1 = 40;
cout << *p1 << endl;
delete t2;
return 0;
}