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;//如果不赋值的话那么编译器会默认给0int 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;
}