前言
接着上篇写。
一、C++面向对象编程
1. 内存四区
① C++类对象中的成员变量和成员函数是分开存储的。
② C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。
③ 静态成员函数、成员变量属于类;
静态成员函数与普通成员函数的区别;
静态成员函数不包含指向具体对象的指针;
普通成员函数包含一个指向具体对象的指针;
2. new和delete
int *p1 = new int; //动态分配4个字节的空间
int *p2 = new int(3); //动态分配4个字节的空间,并初始化为3
int *p3 = new int[3]; //动态分配12个字节的空间
delete p1;
delete p2;
delete[] p3;
3. malloc/free和new/delete的联系
① malloc/free只是动态分配内存空间/释放空间。而new/delete除了非陪空间还会调用构造函数和析构函数进行初始化与清理成员;
② 都是动态管理内存的入口;
③ malloc/free是C/C++标准库的函数,new/delete是C++操作符;
④ malloc/free需要手动计算类型大小返回值为void*,new/delete可自动计算类型大小,返回类型的指针;
⑤ malloc/free管理内存失败会返回NULL,new/delete管理内存失败会抛出异常。
4. 探究new/delete
① 接口原型:
void *operator new (size_t size);
void operator delete(size_t size);
void *operator new[](size_t, size);
void operator delete[](size_t, size);
② new表达式并不直接开辟内存出来,而是通过调用operator new来获得内存,而operator new获得的内存实质上还是用malloc开辟出来的;
③ delete表达式也不是直接去释放内存,实际上delete[]做了这样几件事情:
AA* pA = new AA[10];
delete[] pA;
a. 依次调用pA指向对象数组中的每个对象的析构函数,共10次;
b. 调用operator delete[] (), 它将再调用operator delete;
c. 底层用free执行operator delete表达式,依次释放内存。
5. new/delete和malloc/free混合使用
a. malloc/delete
AA* p1 = (AA*)malloc(sizeof(AA));
delete p1;//没用报错,但是不建议使用,容易引起混肴
AA* p2 = (AA*)malloc(sizeof(AA));
delete[] p2;//报错
b. delete,delete[]之间混肴
AA* p3 = new AA;
free(p3);//不报错,但未清理干净,p3构造函数开辟的空间没有被释放
AA* p4 = new AA[10];
delete p4;//崩溃卡死,释放位置被后移4字节。同时只调用了一次析构函数
AA* p5 = new AA;
delete[] p5;//报错,非法访问内存
二、操作符重载
1.操作符重载基础
运算符函数是一种特殊的成员函数或友元函数,重载为成员函数,解释为:
ObjectL.operator op(ObjectR)
左操作数由Object L通过this指针传递,右操作数由参数ObjectR传递重载为友元函数,解释为:
operator op(ObjectL, ObjectR)
2. 为什么要有操作符重载
class Complex
{
int a,b;
}
Complex C1, C2;
a = a + b;//int 是基础类型,编译器已经为这些类型提供+ 操作了
C1 = C1 + C2;
//C1的类型是Complex,这种类型是自定义类型,编译器无法知道该如何去假发,此时C++编译器给我们提供了一个机制,实现自定义类型相加
//前置--
Complex& operator--()
{
this->a--;
this->b--;
return *this;
}
//前置++
Complex& operator++()
{
this->a++;
this->b++;
return *this;
}
//后置--
Complex operator--(int)
{
Complex tmp = *this;
this->a--;
this->b--;
return tmp;
}
//后置++
Complex operator++(int)
{
Complex tmp = *this;
this->a++;
this->b++;
return tmp;
}
操作符重载的三个步骤(通过类的成员函数,完成操作符重载)
a. 要承认操作符重载是一个函数,要写函数原型
b. 写出函数调用语言c1.operator-(c2);
c. 完善函数原型。
3. 项目开发操作符重载的难点
int& operator[](int i);
/*
数组操作符的应用场景由两个:
a. 放在等号的右边 int operator[] (int i);
b. 放在等号的左边 int& operator[] (int i);
*/
= 操作符的两个应用场景:
a. p3 = p2; 不需要用operator=这个函数的返回值
Array operator= (Array &p2);
b. p1 = p3 = p2; 需要用operator=这个函数的返回值
Array& operator= (Array &p2);
bool operator==(Array &a2);
bool operator!=(Array &a2);
int& Array::operator[] (int i)
{
return mSpace[i];
}
//Array a3(20);
//a3 = a2;
Array& Array::operator= (Array &a2)
{
printf("Array 执¡ä行D=操¨´作Á¡Â\n");
if (this->mSpace != NULL)
{
delete[] mSpace;
mLength = 0;
}
this->mLength = a2.mLength;
this->mSpace = new int[a2.mLength];
for (int i = 0; i < a2.mLength; i++)
{
mSpace[i] = a2[i];
}
return *this;
}
bool Array::operator==(Array &a2)
{
//length
if (this->mLength != a2.mLength)
{
return false;
}
for (int i = 0; i < a2.mLength; i++)
{
if (this->mSpace[i] != a2[i])
{
return false;
}
}
return true;
}
bool Array::operator!=(Array &a2)
{
return !(*this == a2);
}
三. 继承
1. 继承的基本概念
面向对象中的继承指类之间的父子关系:
a. 子类拥有父类的所有成员变量和成员函数;
b. 子类就是一种特殊的父类;
c. 子类对象可以当作父类对象使用;
d. 子类可以拥有父类没有的方法和属性。
2. 继承中的构造函数和析构函数
C++中子类对外访问属性表,访问级别:
public | protected | private | |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | private | private | private |
代码如下(示例):
3. 继承模型
在子类对象构造的时,需要调用父类构造函数对其继承得来的成员进行初始化;
在子类对象析构的时,需要调用父类析构函数对其继承得来的成员进行清理。
继承与组合混搭情况下,构造和析构调用原则
原则: 先构造父类,再构造成员变量、最后构造自己
先析构自己,在析构成员变量、最后析构父类
#include <iostream>
using namespace std;
//子类对象如何初始化父类成员
//继承中的构造和析构
//继承和组合混搭情况下,构造函数、析构函数调用顺序研究
class Object
{
public:
Object(const char* s)
{
cout << "Object()" << " " << s << endl;
}
~Object()
{
cout << "~Object()" << endl;
}
};
class Parent : public Object
{
public:
Parent(const char* s) : Object(s)
{
cout << "Parent()" << " " << s << endl;
}
~Parent()
{
cout << "~Parent()" << endl;
}
};
class Child : public Parent
{
protected:
Object o1;
Object o2;
public:
Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!")
{
cout << "Child()" << endl;
}
~Child()
{
cout << "~Child()" << endl;
}
};
void run()
{
Child child;
}
int main(void)
{
run();
system("pause");
return 0;
}
四. 多态
1. 多态的基本概念
调用同样的语句有多种不同的表现形态。
2. 多态成立的三个条件
a. 要有继承;
b. 要有函数重写,含virtual关键字;
c. 父类指针(父类引用)指向子类对象。
3. 函数重载与函数重写
函数重载:
必须在同一个类中进行;
子类无法重载父类的函数,父类同名函数将被名称覆盖;
重载是在编译期间根据参数类型和个数决定函数调用。
函数重写:
必须发生于父类与子类之间;
并且父类与子类中的函数必须由完全相同的原型;
使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义);
多态是在运行期间根据具体对象的类型决定函数调用。
4. 多态原理探究
当类中声明虚函数时,编译器会在类中生成一个虚函数表,
虚函数表是一个存储类成员函数指针的数据结构,
虚函数表是由编译器自动生成与维护的,
virtual成员函数会被编译器放入虚函数表中,
存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
虚函数表指针(vptr)被编译器初始化的过程:
a. vptr指针是分布完成的:
首先调用父类的构造函数,初始化子类的vptr指针;
b. 如果在父类里面调用多态函数,此时,子类的vptr指针正好指向父类的虚函数表,所以产生不了多态;
c. 只有当对象的构造完全结束后vptr的指向才最终确定。
#include <iostream>
using namespace std;
class Parent01
{
protected:
int i;
int j;
public:
virtual void f()
{
cout << "Parent01::f" << endl;
}
};
class Child01 : public Parent01
{
public:
int k;
public:
Child01(int i, int j)
{
printf("Child01:...do\n");
}
virtual void f()
{
printf("Child01::f()...do\n");
}
};
void howToF(Parent01 *pBase)
{
pBase->f();
}
//多态是靠迟绑定实现的(vptr+函数指针实现)
int main()
{
int i = 0;
Parent01* p = NULL;
Child01* c = NULL;
//不要把父类对象还有子类对象同事放在一个数组里面
Child01 ca[3] = { Child01(1, 2), Child01(3, 4), Child01(5, 6) };
//不要用父类指针做赋值指针变量,去遍历一个子类的数组。
//把数组的首地址,赋给基类指针
p = ca;
//把数组的首地址,赋给子类指针
c = ca;
p->f();
c->f(); //有多态发生
// p++;
// c++;
//
// p->f();//有多态发生
// c->f();
for (i = 0; i < 3; i++)
{
howToF(&(ca[i]));
}
system("pause");
return 0;
}
5. 为什么要在析构函数前加virtual
class AA
{
public:
AA(int a= 0)
{
this->a = a;
print();
}
virtual ~AA()
{
cout<<"父类析构函数do"<<endl;
}
virtual void print()
{
cout<<"父类的"<<"a"<<a<<endl;
}
protected:
int a ;
};
class BB : public AA
{
public:
BB(int a= 0, int b = 0)
{
this->a = a;
this->b = b;
}
~BB()
{
cout<<"子类析构函数do"<<endl;
}
virtual void print()
{
cout<<"子类的"<<"a"<<a<<"b"<<b<<endl;
}
private:
int b ;
};
//如果想通过父类指针 执行 所有的子类对象的析构函数,那么需要在父类析构函数前加上virtual关键字
//把父类的析构函数变成虚析构函数
void howToDelete(AA *pBase)
{
//
delete pBase;
}
void main()
{
BB *b1 = new BB(1, 2);
b1->print();
howToDelete(b1);
//子类对象的时候,
//delete b1;
system("pause");
}
五. 抽象类(接口类)
绝大多数面向对象语言都不支持多继承;
绝大多数面向对象语言都支持接口类的概念;
C++中没有接口的概念;
C++中可以使用纯虚函数实现接口;
接口类中只有函数原型定义,没有任何数据的定义。
1. 纯虚函数和接口类基础
class SocketIF
{
public:
//客户端初始化 获取handle 上下文信息
virtual int cltSocketInit() = 0;
//客户端发报文
virtual int cltSocketSend( unsigned char *buf , int buflen ) = 0;
//客户端收报文
virtual int cltSocketRev( unsigned char *buf , int *buflen ) = 0;
//客户端释放资源
virtual int cltSocketDestory() = 0;
public:
virtual ~SocketIF()
{
}
};
class SocketImp1 :public SocketIF
{
public:
SocketImp1(void);
~SocketImp1(void);
public:
int cltSocketInit();
//客户端发报文
int cltSocketSend( unsigned char *buf , int buflen );
//客户端收报文
int cltSocketRev( unsigned char *buf , int *buflen );
//客户端释放资源
int cltSocketDestory();
private:
unsigned char *buf;
int buflen;
};
C++类和函数的区别(为什么没有handle)
六. 函数指针
1. 语法基础
//定义一个函数类型
typedef int Func(int);
Func MyFunc;
//定义一个指向函数类型的指针类型
typedef int(*MyPFun)(int);
MyPFun pMyFunc;
//直接定义一个函数指针,并且赋值
void (*myf1)() = NULL;
//函数名称就代表函数的入口地址, 函数名称本身就是一个指针
//可以把函数名赋给一个函数指针,通过函数指针进行函数调用
int test(int a)
{
return a*a;
}
//语法基础
//函数指针做函数参数的两种写法
//第一种写法:
int add(int a, int b);
//第二个函数 是函数指针 做函数参数
//在这个函数里面,就可以通过这个函数指针,去调用外部的函数,形成一个回调
int libFun( int (*pDis)(int a, int b));
typedef int (*MyPFunDemo)(int);
MyPFunDemo pMyFunc;
2. 正向调用
//客户端初始化 获取handle上下
typedef int (*CltSocketInit)(void **handle );
//客户端发报文
typedef int (*CltSocketSend)(void *handle , unsigned char *buf , int buflen );
//客户端收报文
typedef int (*CltSocketRev)(void *handle , unsigned char *buf , int *buflen );
//客户端释放资源
typedef int (*CltSocketDestory)(void *handle);
//在这个函数里面完成动态库的加载
//利用winapi
void CMFC应用程序动态加载dll项目Dlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
HINSTANCE hInstance = NULL;
hInstance=::LoadLibrary("c:/socketclient.dll");
CltSocketInit cltSocketInit = (CltSocketInit)::GetProcAddress(hInstance, "cltSocketInit");
CltSocketSend cltSocketSend = (CltSocketSend)::GetProcAddress(hInstance, "cltSocketSend");
CltSocketRev cltSocketRev = (CltSocketRev)::GetProcAddress(hInstance, "cltSocketRev");
CltSocketDestory cltSocketDestory = (CltSocketDestory)::GetProcAddress(hInstance, "cltSocketDestory");
void *handle = NULL;
unsigned char buf[100];
int buflen = 10;
memcpy(buf, "ddddddddddssssssssss", 10);
unsigned char out[100] = {0};
int outlen = 0;
int ret = cltSocketInit(&handle);
ret = cltSocketSend(handle, buf, buflen);
ret = cltSocketRev(handle, out, &outlen);
ret = cltSocketDestory(handle);
printf("out:%s", out);
//SQRTPROC* pFunction;
//VERIFY(hInstance=::LoadLibrary("c:\\winnt\\system32\\mydll.dll"));
//VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
//double d=(*pFunction)(81.0);//调用该DLL函数
AfxMessageBox("dddd");
}
3. 反向调用
回调函数是利用函数指针实现的一种调用机制
回调机制原理
当具体事件发生时,调用者通过函数指针调用具体函数
回调机制的将调用者和被调函数分开,两者互不依赖
任务的实现 和 任务的调用 可以耦合 (提前进行接口的封装和设计)
七. 泛型编程
1. 泛型编程语法基础
template<typename T>
void swap2(T &a, T &b)
{
T c;
c = a;
a = b;
b = c;
}
void main()
{
//泛型编程的调用方式有两种
//自动类型推导
int x = 1, y = 2;
swap2(x, y);
printf("x:%d y:%d \n", x, y);
float x1 = 1.0, y1 = 2.0;
//具体类型调用
swap2<float>(x1, y1);
printf("x1:%f y1:%f \n", x1, y1);
system("pause");
}
2. 模板编程
template<class T>
void printfArray(T *a, int num)
{
cout<<endl;
for (int i=0; i<num; i++)
{
cout<<a[i]<<" ";
}
}
/*
1 函数模板可以像普通函数一样被重载
2 C++编译器优先考虑普通函数
3 如果函数模板可以产生一个更好的匹配,那么选择模板
4 可以通过空模板实参列表的语法限定编译器只通过模板匹配
*/
/*
函数模板不允许自动类型转化
普通函数能够进行自动类型转换
*/
/*
函数模板的深入理
― 编译器并不是把函数模板处理成能够处理任意类型的函数
― 编译器从函数模板通过具体类型产生不同的函数
― 编译器会对函数模板进行两次编译
―在声明的地方对模板代码本身进行编译
―在调用的地方对参数替换后的代码进行编译
*/
总结
C++基础先复习到这儿,后期继续添加。