C++面向对象编程
面向对象基础
实体(属性,行为) ADT(abstract data type)
面向对象语言的四大特征:抽象,封装(隐藏),继承,多态。
访问限定符:public
共有的,private
私有的, protected
保护的
类的成员函数和内联函数inline的区别?
一个类的成员方法,在类内声明和定义,那么它本身就是inline
函数。如果类的成员方法在类内声明,类外定义,且没有关键字inline
修饰,那么该方法就是类的普通的函数。如果在类外定义类的成员方法,并且用inline
关键字修饰为内联函数,则该函数必须写在和声名在一个头文件中,,否则编译就会出现问题。具体查看inline 成员函数。
有无inline
修饰的区别在于:普通函数调用需要先开辟形参变量内存空间,在运行函数主体,结束时再回收空间。而inline
修饰后可以类似于将函数体直接转在调用点后面,就节省了函数调用内存空间的开辟和回收。但是inline
是否起作用,还需要看编译器是否将其编译为内敛函数,如果太过于复杂的函数体,那么极大可能就不会成为内联函数。具体可以查看C++函数调用那些事。
C++对象内存大小的计算方法:
对象的内存大小只和成员变量有关,和成员方法无关。
可以使用vs中的终端,使用命令cl xxx.cpp /d1eportSingleClassLayoutXXX
查看xxx.cpp
下XXX
类的内存大小。
#include<iostream>
#include<string>
using namespace std;
const int LEN = 20;
class TestObj
{
public:
void setX(int n);
void setY(int n);
void setName(const char* name);
private:
int x;
int y;
char name[LEN];
};
int main()
{
return 0;
}
C++类对象属性是独立的,也就是各个对象的成员属性是单独的,互不影响。但是类的成员方法是公用的,类对象都可以访问,编译后都放在代码段。
类的成员方法一经过编译,所有的方法参数,都会加一个this指针,接收调用该方法的对象的地址。
类的构造函数和析构函数
没有提供任何构造函数的时候,编译器会自动生成默认构造和默认析构,是空函数(函数里面没有函数体部分)。自己定义了构造函数后,编译器就不会在创建默认构造函数。
对象析构的顺序和创建的顺序相反:后构造的先析构。
//构造函数和析构函数,实现一个顺序栈
//构造函数和析构函数和类名一样,没有返回值
#include<iostream>
using namespace std;
//构造函数和析构函数,实现一个顺序栈
//构造函数和析构函数和类名一样,没有返回值
class SeqStack
{
public:
void init(int size = 10)
{
_pstack = new int[size];
_top = -1;
_size = size;
}
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 release()
{
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 = _size * 2;
}
};
int main()
{
SeqStack s;
//s.init(5); //空间初始化,现在用构造函数代替,初始化操作容易被忘记
for (int i = 0; i < 15; i++)
{
s.push(i);
}
while (!s.empty())
{
cout << s.top() << " ";
s.pop();
}
cout << endl;
//s.release();//堆空间析构,现在用析构函数代替,因为空间释放很容易被忘记
SeqStack s2(50);
//s2.~SeqStack();//自己调用析构函数释放资源,一般不自己写调用析构函数
SeqStack s3=new SeqStack();//malloc内存开辟,在使用构造函数进行对象初始化
//必须显式回收s3的内存资源,否则作用域结束后也不会析构,因为new创建的对象存储在堆区
delete s3;//先执行析构s3.~SeqStack();在执行free(ps);
return 0;
}
对象的深拷贝和浅拷贝
对象的默认拷贝构造函数是做内存的数据拷贝,关键是对象如果占用外部资源(使用堆空间),那么浅拷贝就会出现问题。
所以在类创建的对象存在外部资源访问的情况下,需要重新定义类的默认拷贝构造函数和operator=函数,否则可能会出现对象析构时出现释放野指针的情况。
还是以SeqStack
为例,主要看拷贝构造函数和operator=函数,以及main函数的注释代码。
#include<iostream>
using namespace std;
//构造函数和析构函数,实现一个顺序栈
//构造函数和析构函数和类名一样,没有返回值
class SeqStack
{
public:
void init(int size = 10)
{
_pstack = new int[size];
_top = -1;
_size = size;
}
SeqStack(int size=10)
{
cout << this << "SeqStack" << endl;
_pstack = new int[size];
_top = -1;
_size = size;
}
SeqStack(const SeqStack& src)
{
/*
//默认的拷贝构造函数长这样
_pstack = src._pstack;//指针赋值,也就是将新的对象的_pstack也指向原来的src._pstack的地址
_top = src._top;
_size = src._size;
*/
cout << "SeqStack(const SeqStack&)" << 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=" << endl;
//需要先释放当前对象的内存,防止内存丢失(因为采用=重载,左边的对象一般存在空间)
//防止自己给自己赋值
if (this == &src)
{
return;
}
delete[]_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 release()
{
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 = _size * 2;
}
};
int main()
{
SeqStack s1(10);
/*
调用默认拷贝构造函数进行对象s2的实例化,这里会存在一个严重的问题,因为SeqStack会开辟一个堆空间,
调用默认拷贝构造函数会导致s2和s1共用一个堆内存空间,在s2析构完后会将空间释放,
而s1在释放该内存就会出现内存访问异常
*/
//SeqStack s2 = s1;//这里使用默认拷贝构造会导致问题,具体查看拷贝构造函数
//SeqStack s3(s1);//其实和上面SeqStack s2 = s1;一样,也是调用拷贝构造函数
SeqStack s2 = s1;
//s2.operator=()
s2 = s1;//如果对象没有重写=操作符函数,那么默认就是做浅拷贝,内存数据的拷贝,类似默认拷贝构造函数
//而且这里会存在一个内存泄漏的严重问题,将s1._pstack -> s2._pstack导致原来的s2._pstack没有释放就被丢弃了
//这里也会出现内存访问异常,想再次释放空间的错误,释放野指针
return 0;
}
拷贝构造函数和赋值操作符重载实践
自定义实现String
#include<iostream>
#include<string>
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] = '\0';
}
}
String(const String& other)
{
m_data = new char[strlen(other.m_data) + 1];
strcpy(m_data, other.m_data);
}
~String()
{
delete[]m_data;
m_data = nullptr;
}
//String& 是为了实现多个后续情况 str1=str2=str3;
String& operator=(const String& 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()
{
//调用String(const char* str = nullptr)构造函数
String str1;
String str2("hello");
String str3 = "hello";
//调用拷贝构造函数
String str4(str3);
String str5 = str3;
//赋值操作符重载
str1 = str2;
return 0;
}
自定义实现循环Queue
#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 = src._front; i != _rear;i=(i+1)%_size)
{
_pQue[i] = src._pQue[i];
}
}
Queue& operator=(const Queue&src)
{
if(this==&src)
{
return *this;
}
delete[] _pQue;
_front = src._front;
_rear = src._rear;
_size = src._size;
_pQue = new int[_size];
for (int i = src._front; i != _rear; i = (i + 1) % _size)
{
_pQue[i] = src._pQue[i];
}
}
~Queue()
{
delete[] _pQue;
_pQue = nullptr;
}
void push(int val)
{
if (full())
resize();
_pQue[_rear++] = val;
_rear = _rear % _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 _rear == _front;
}
private:
int *_pQue; // 申请队列的空间数组
int _front; // 指示队头的位置
int _rear; // 指示队尾的位置
int _size;
void resize()
{
int *temp = new int[_size * 2];
int index = 0;
for (int i = _front; i != _rear; i = (i + 1) % _size)
{
temp[index++] = _pQue[i];
}
delete[] _pQue;
_pQue = temp;
_front = 0;
_rear = index;
_size = 2 * _size;
}
};
int main()
{
Queue q;
for (int i = 0; i < 30; i++)
{
q.push(i);
}
while (!q.empty())
{
cout << q.front() << " ";
q.pop();
}
Queue m = q;
m = q;
return 0;
}
构造函数的初始化列表
使用类的对象作为一个类的成员变量,这里举了一个商品和事件的示例:主要看Goods类构造函数的解析
class Goods
{
public:
Goods(const char* n, int a, int p, int y, int m, int d)
:_data(y, m, d) //Data _data(y,m,d); 这里会调用Data的构造函数,也称为构造函数的初始化列表 #step1先执行
//,_amount(a) //int _amount=a;
{
// #step2 后执行
strcpy(_name, n);
_amount = a; //int _amount;_amount=a;
_price = p;
}
void show()
{
cout << "name: " << _name << endl;
cout << "amount: " << _amount << endl;
cout << "price: " << _price << endl;
_data.show();
}
private:
char _name[20];
int _amount;
double _price;
Data _data;//如果没有默认构造函数就会初始化错误,没有构造函数可用
};
int main()
{
Goods g("面包", 100, 25.0, 2023, 8, 1);
g.show();
Test t;
t.show();
return 0;
}
成员变量的初始化顺序和它们定义的顺序有关,和构造函数的初始化列表中出现的先后顺序无关。
class Test
{
public:
Test(int data = 10) :mb(data), ma(mb) {};//这里初始化的顺序按照定义的顺序,ma先初始化,mb后初始化
void show() { cout << "ma: " << ma << "mb: " << mb << endl; } //ma: -858993460mb: 10
private:
//成员变量的初始化顺序和它们定义的顺序有关,和构造函数的初始化列表中出现的先后顺序无关。
int ma;
int mb;
};
类的各种成员
类的各种成员方法,包括类的成员方法和类的成员变量。
普通成员变量和普通成员方法都依赖于对象,适用对象进行访问和调用(这是因为普通成员方法都会产生一个this指针)。
静态成员变量和方法,都没有this指针,不需要依赖对象调用。类中有静态成员变量时,必须要类内声明,类外定义(如果不赋初值,则默认就是0)。静态成员变量的一些用途有:可以在每次调用构造函数的时候,让静态成员变量+1,相当于可以计数使用。静态成员方法的一些用途:可以定义静态成员方法,直接使用类名+静态成员方法名访问静态成员变量,获得计数的个数。
普通成员方法: 编译器会添加一个this形参变量
- 属于类的作用域。
- 调用该方法时,需要依赖一个对象。
- 可以任意访问对象的私有成员,protected 继承 public private
static静态成员方法:编译器不会添加this形参变量
- 属于类的作用域。
- 可以使用类名作用域来调用方法。
- 可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其他的static静态成员)
常成员方法:
- 属于类的作用域。
- 调用该方法时,需要依赖一个普通对象(类型访问)或者常对象,如下代码所示。
- 可以任意访问对象的私有成员,只能读,不能写。
- 只要是只读操作的成员方法,最好实现为常成员方法,防止错误。
class Test
{
public:
void func() { cout << "call Test::func" << endl; }
static void static_func()
{
cout << "call Test::static_func" << endl;
cout << mb << endl;
// cout << ma << endl; //错误,因为调用了非静态成员变量,静态成员函数不会产生this指针
}
void show() //Test *this
{
cout<<ma<<endl;
}
void show()const //const Test *this,构成函数重载,一般只读的方法设置为常成员方法
{
cout<<ma<<endl;
}
int ma;
static int mb;//静态成员方法必须在类内声明,类外定义,初始化
};
int Test::mb = 30;
int main()
{
const Test t();
t.show();//不能调用show()方法,因为是调用的时候传入的是const Test *this,
//而普通的show方法的参数是Test *this,不能将Test *this <- const Test *this转化
//所以需要添加一个void show() const{}方法,也成为常方法,和void show()构成函数重载
//加了const的show方法,传入的this指针式const的指针,所以参数列表不一样
}
指向类成员的指针
指向类成员(成员变量和成员方法)的指针。
指向成员变量的指针:
//指向类成员(成员变量和成员方法)的指针
class Test
{
public:
void func() { cout << "call Test::func" << endl; }
static void static_func() { cout << "call Test::static_func" << endl; }
int ma;
static int mb;//静态成员方法必须在类内声明,类外定义,初始化,不录入对象内存中
};
int Test::mb = 30;//如果不赋值的话那么编译器会默认给0
int main()
{
//int a = 10; int* p = &a; *p = 30; //可以通过普通的指针变量修改值
//int Test::*p = &Test::ma; //但是对于普通成员而言,没有对象直接谈论其成员变量是没有意义的
Test t1;
Test* t2 = new Test();
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;
}
指向类的成员方法指针:
//指向类成员(成员变量和成员方法)的指针
class Test
{
public:
void func() { cout << "call Test::func" << endl; }
static void static_func() { cout << "call Test::static_func" << endl; }
int ma;
static int mb;//静态成员方法必须在类内声明,类外定义,初始化
};
int Test::mb = 30;
int main()
{
Test t1;
Test* t2 = new Test();
//C语言定义的函数指针严重性,无法从“void(__thiscall Test::*)(void)”转换为“void(__cdecl*)(void)
//void(*pfunc)() = &Test::func;
void(Test:: * pfunc)() = &Test::func;
(t1.*pfunc)();
(t2->*pfunc)();
//static修饰的成员方法不依赖于对象,其地址就是普通地址,不需要加类作用域
void(* psfunc)() = &Test::static_func;
(*psfunc)();//静态方法不依赖于对象调用
delete t2;
return 0;
}