本文共 17094 字,大约阅读时间需要 56 分钟。
C风格转换【(TYPE)EXPRESSION】可在任意类型之间转换,且不易查找。所以C++引进了四种类型转换操作符,解决以上问题。
类型 | 主要用途 |
---|---|
static_cast | 静态类型转换 |
dynamic_cast | 子类和父类间多态类型转换 |
const_cast | 去掉const属性转换 |
reinterpreter_cast | 重新解释类型转换 |
#includeint main(){ double pi = 3.1415926; int num1 = (int)pi; // C语言旧式类型转换 int num2 = pi; // 隐式类型转换 //C++静态类型转换 //在编译期完成基本类型的转换,且有类型检查 int num3 = static_cast (pi); std::cout << "num1 = " << num1 << std::endl; std::cout << "num2 = " << num2 << std::endl; std::cout << "num3 = " << num3 << std::endl; system("pause"); return 0;}
#includeclass Animal{public: virtual void shout() = 0;};class Dog : public Animal{public: virtual void shout(){ std::cout << "汪汪" << std::endl; } void dohome() { std::cout << "看家" << std::endl; }};class Cat : public Animal{public: virtual void shout(){ std::cout << "喵喵" << std::endl; } void dohome() { std::cout << "抓老鼠" << std::endl; }};int main(){ Animal* base = NULL; base = new Cat(); base->shout(); //将父类指针转成子类 //此时转换失败,因为父类指针现在指向的对象是猫 //转换失败返回空(NULL) Dog* dog = dynamic_cast (base); if (NULL!=dog) { dog->shout(); dog->dohome(); } //此时转换成功,成功将父类指针转换成子类指针 Cat* cat = dynamic_cast (base); if (cat != NULL){ cat->shout(); cat->dohome(); } system("pause"); return 0;}
const_cast<目标类型>(标识符):目标类型只能是指针或者引用
#includeclass A{public: int data;};int main{ //类似于结构体的初始化方式 const A a = { 200 }; //编译失败:const_cast 目标类型只能是引用或者指针 //A a1 = const_cast (a); A& a2 = const_cast (a); a2.data = 100; std::cout << &a << "——" << a.data << std::endl; std::cout << &a2 << "——" << a2.data << std::endl; A* a3 = const_cast (&a2); a3->data = 100; std::cout << &a3 << "——" << a3->data << std::endl; system("pause"); return 0;}
扩展:取址符号的解释
#include//& 表示取址符号,取的是当前变量本身的内存地址int a = 1; // 变量aint &c = a; // 引用cint *b = &a; // 指针bint main(){ std::cout << &c << std::endl; //输出的是c的存储地址也就是a的地址 std::cout << *&c << std::endl; //*取这个地址的值,输出1 std::cout << b << std::endl; //输出指针指向的地址,即a的地址 std::cout << &b << std::endl; //取的是指针b本身的地址 system("pause"); return 0;}
reinterpret_cast是四种强制转换中功能最强大的,它可以暴力完成两个完全无关类型的指针之间或指针和数之间的互转,比如用char类型指针指向double值。它对原始对象的位模式提供较低层次上的重新解释(即reinterpret),完全复制二进制比特位到目标对象,转换后的值与原始对象无关但比特位一致,前后无精度损失
指针和数之间的互转
#includeint main(){ double d = 12.1; char* p = reinterpret_cast (&d); std::cout << "*p的值:" << *p << std::endl; // 将d以二进制(位模式)方式解释为char,并赋给*p double* q = reinterpret_cast (p); std::cout << "*q的值:" << *q << std::endl; // 12.1}
无关类型指针之间的类型转换
#includeclass Person{public: void run(){ std::cout << "我在不停的奔跑" << std::endl; }};class Sky{public: void laugh(){ std::cout << "天空乌云密布,暴雨要来了" << std::endl; }};int main(){ Person* p = new Person(); p->run(); Sky* sky = reinterpret_cast (p); sky->laugh(); std::cout << &p << "——" << std::endl; std::cout << &sky << "——" << std::endl; system("pause"); return 0;}
在C++中,程序在内存中的存储被分为五个区:
1、代码区(Text):只读,共享,静态分配,存放程序执行代码,也可能包含常量
2、全局区:编译时分配内存,程序结束后由系统释放,存放全局变量和静态变量
3、栈区(stack):由编译器自动分配释放 ,存放函数参数值,局部变量值等,按内存地址由高到低方向生长,其空间大小由编译时确定,速度快,空间小
4、堆区(heap):由程序员分配释放,按内存地址由低到高方向生长,其空间大小由系统内存上限决定,速度慢,空间大
每个线程都会有自己的栈,但是堆空间是共用的,下图为C++内存区域划分图
区域类型 | 列举场景 |
---|---|
代码区 | 类的成员函数和全局函数 |
全局区 | 全局变量和静态变量 |
栈区 | 局部变量和函数参数值 |
堆区 | 调用malloc函数或new运算符重载申请空间 |
类型 | 内存划分 | 作用范围 | 生命周期 |
---|---|---|---|
局部变量 | 栈 | 定义局部变量所在的大括号内 | 定义的函数被调用时,在栈开辟空间,函数调用结束,被清除 |
全局变量 | 全局区 | 所有源文件 | 在编译期即分配好了内存空间,在程序运行结束时,被清除 |
静态局部变量 | 全局区 | 定义局部变量所在的大括号内 | 在编译期即分配好了内存空间,在程序运行结束时,被清除 |
静态全局变量 | 全局区 | 只能在定义的文件中调用 | 在编译期即分配好了内存空间,在程序运行结束时,被清除 |
避免变量或函数重命名,命名空间可以将多个变量和函数等包含在内,使其不会与命名空间外的任何变量和函数等发生重命名的冲突
namespace ride{ void PrintNum(int x, int y){ std::cout << x*y << std::endl; }}//打印两数相乘的命名空间namespace plus{ void PrintNum(int x, int y){ std::cout << x+y << std::endl; }}//打印两数相加的命名空间int main(){ ride::PrintNum(1, 4); plus::PrintNum(1, 4); system("pause"); return 0;}
例一
void GetMemory(char *p){ p = (char *)malloc(100);}void Test(void){ char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str);}
结果:编译成功,运行失败
原因:因为调用GetMemory相当于值传递,str指针实际上并没有分配到内存空间,访问空指针会运行失败,因为在C++中内存编号0 ~255为系统占用内存,内存地址编号为0的空间不允许用户访问
详细过程:在函数地址入栈后,p作为形参,只是拷贝了一份str的地址,并为该地址申请了空间,但是str指向的地址仍然是NULL
解决方法一
void GetMemory(char **p){ *p = (char *)malloc(100);}void Test(void){ char *str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str);}
解决方法二
void GetMemory(char* &p){ p = (char *)malloc(100);}void Test(void){ char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str);}
例二
char *GetMemory(void){ char p[] = "hello world"; return p;}void Test(void){ char *str = NULL; str = GetMemory(); printf(str);}
结果:打印结果不确定
原因:调用函数返回值是局部变量的指针,局部变量在函数被调用结束后,在栈区的内存空间被释放。因此str在打印时,指针指向的是非法(不可控)的内存地址,即野指针,在C++中访问野指针的结果是不确定的
例三
void GetMemory2(char **p, int num){ *p = (char *)malloc(num);}void Test(void){ char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str);}
结果:正常打印hello
原因:地址传递,把str指针的地址当做实参传递给形参,并在GetMemory2中为该地址申请地址空间
例四
void Test(void){ char *str = (char *) malloc(100); strcpy(str, “hello”); free(str); if(str != NULL) { strcpy(str, “world”); printf(str); }}
结果:运行时中断,后打印world
原因:free只是释放了malloc所申请的内存,并没有改变指针的值,因此由于指针所指向的内存已经被释放,所以其它代码有机会改写其内容,相当于该指针指向了自己无法控制的地方,也称为野指针。为了避免操作野指针,在free后应将指针指向NULL
函数指针:指向函数的指针,可通过指针访问函数,函数名作为实参使用时
作用:通过使用函数指针可以简化代码,在一定程度上提高代码可读性
#includeint add(int a, int b){ return a + b;}typedef int(*FucPoint)(int, int);//使用函数指针作为形参void myFun(int a, int b, FucPoint p){ p(a, b);}int main(){ //函数名作为入参时,C++会自动将其转换为函数指针 myFun(2, 3, add); system("pause"); return 0;}
静态函数:被static关键字修饰的函数,静态成员函数属于类,不创建对象也可调用。类成员函数与静态成员函数的区别在于普通成员函数有一个隐藏的调用参数(this)指针
应用场景:从面向对象的角度上来说,在抉择使用实例化方法或静态方法时,应该根据是否该方法和实例化对象具有逻辑上的相关性,如果是就应该使用实例化对象,反之使用静态方法
要求:
- 创建一个无序的数组,通过定义的排序算法函数sort排序并输出
- 排序算法函数sort使用排序规则回调函数(函数指针)作为参数传入
#includebool judge(int a, int b){ return a>b?true:false;}typedef bool(*FucPoint)(int, int);class SortUtil{public: static int* sort(int arr[], int len, FucPoint p){ for (int i = 0; i < len - 1; ++i) { for (int j = 0; j < len - 1 - i; ++j) { if (p(arr[j] , arr[j + 1])) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } return arr; }};int main() { int arr[9] = { 4, 2, 8, 0, 5, 7, 1, 3, 9 }; int len = sizeof(arr) / sizeof(int); SortUtil::sort(arr,len,judge); system("pause"); return 0;}
要求:实现类的无参构造、有参构造、拷贝构造、拷贝复制、析构函数
#includeclass Person {public: Person() { } Person(int age, int height) { age_ = age; height_ = new int(height); } Person(const Person& p) { age_ = p.age_; height_ = new int(*p.height_); } virtual ~Person() { if (height_ != NULL) { delete height_; } }private: int age_; int* height_;};
扩展:构造函数初始化列表与构造函数中的赋值的区别
测试代码如下
#includeclass A{public: A(){ std::cout << "无参构造" << std::endl; } A(const A& a){ std::cout << "拷贝构造" << std::endl; } A& operator = (const A &a){ std::cout << "运算符重载" << std::endl; return *this; }};class B{public: B(const A& val) :m_b_(val){ std::cout << "参数列表初始化" << std::endl; }private: A m_b_;};class C {public: C(const A& c) { m_c_ = c; std::cout << "构造函数内赋值" << std::endl; }private: A m_c_;};int main(){ A a; system("pause"); B b(a); system("pause"); C c(a); system("pause"); return 0;}
程序执行结果如图所示:
执行B b(a) ,直接调用A(const A& a)拷贝构造函数完成初始化
执行C c(a) ,先调用A对象的无参构造函数,后调用A对象的重载运算符函数
A& operator = (A &a)
结论:不同的初始化方式得到不同的结果,构造函数内赋值初始化多调用了一次构造函数,因此构造函数初始化列表的方式效率更高
定义一个圆形类Circle
要求:包含 私有成员(半径),公有函数成员(设置半径、计算面积、计算周长),构造函数(无参构造、有参数构造、拷贝构造)
主函数使用圆形类Circle创建圆形对象
- 创建圆对象c1,键盘输入
x
,设定为c1半径,计算并显示c1面积和周长- 创建圆对象c2,初始化半径为
2x
,计算并显示c2面积和周长- 创建圆对象c3,并用c1初始化c3,计算并显示c3面积和周长
Math.h中有下面一段:
#if defined(_USE_MATH_DEFINES) && !defined(_MATH_DEFINES_DEFINED)#define _MATH_DEFINES_DEFINED#define M_E 2.71828182845904523536#define M_LOG2E 1.44269504088896340736#define M_LOG10E 0.434294481903251827651#define M_LN2 0.693147180559945309417#define M_LN10 2.30258509299404568402#define M_PI 3.14159265358979323846#define M_PI_2 1.57079632679489661923#define M_PI_4 0.785398163397448309616#define M_1_PI 0.318309886183790671538#define M_2_PI 0.636619772367581343076#define M_2_SQRTPI 1.12837916709551257390#define M_SQRT2 1.41421356237309504880#define M_SQRT1_2 0.707106781186547524401#endif /* _USE_MATH_DEFINES */
圆形类代码实现
#include#define _USE_MATH_DEFINES#include class Circle{public: //被const修饰的成员函数不能引用非const函数,也不能修改类内的变量值,非常安全 void setRadius(const double r){ radius_ = r; } double getPerimeter() const{ return 2 * M_PI*radius_; } double getArea() const{ return M_PI*radius_*radius_; } Circle() { } explicit Circle(double r) { radius_ = r; } Circle(const Circle& c) { radius_ = c.radius_; }private: double radius_;};int main() { Circle c1; double x; std::cout << "请输入圆形的半径" << std::endl; std::cin >> x; c1.setRadius(x); double c1_perimeter = c1.getPerimeter(); double c1_area = c1.getArea(); std::cout << "c1的周长为:" << c1_perimeter << std::endl; std::cout << "c1的面积为:" << c1_area << std::endl; Circle c2(2 * x); double c2_perimeter = c2.getPerimeter(); double c2_area = c2.getArea(); std::cout << "c2的周长为:" << c2_perimeter << std::endl; std::cout << "c2的面积为:" << c2_area << std::endl; Circle c3 = c1; double c3_perimeter = c3.getPerimeter(); double c3_area = c3.getArea(); std::cout << "c3的周长为:" << c3_perimeter << std::endl; std::cout << "c3的面积为:" << c3_area << std::endl; system("pause"); return 0;}
要求:编写Integer
类使下列代码输出结果为9
int i=2;int j=7;Integer x(i);Integer y(j);cout<<(x+y)<
代码如下:
#includeclass Integer{private: int value_;public: Integer(int v){ value_ = v; } Integer& operator + (const Integer& integer){ this->value_ += integer.value_; return *this; }; int getValue() const{ return this->value_; };};std::ostream& operator<< (std::ostream& os, const Integer& integer){ os << integer.getValue(); return os;}int main(){ int i = 2; int j = 7; Integer x(i); Integer y(j); std::cout << (x + y) << std::endl; system("pause"); return 0;}
扩展:算符左++和右++
对于++有两种方式,那么在重载++的时候要怎么区分:
//前置:++iT& operator++(){ //do something return *this;}//后置:i++const T operator++(int){ T tmp = *this; //do something return tmp;}
为什么前置++可以返回当前对象的引用而后置++不可以?
因为前置加加是可以允许外部对该对象做修改(它先自增,再返回当前的值)
而后置加加是先返回当前对象的值才自增,故不能返回引用
区别主要在:1、返回值 2、函数参数
代码实现:
#includeclass Rectangle{public: Rectangle(const int w, const int h) : width_(w), height_(h) {}; ~Rectangle() {}; // ++i Rectangle& operator++ () { this->height_ += 1; this->width_ += 1; return *this; } // i++ //返回类型不能是引用 const Rectangle operator++ (int) { //Rectangle temp = *this; Rectangle temp(*this); this->height_ += 1; this->width_ += 1; return temp; } void showVal(){ std::cout << width_ << "---" << height_ << std::endl; }private: int width_; int height_;};int main(){ Rectangle rct1(40, 10); Rectangle rct2 = (rct1++); rct1.showVal();// 输出 41, 11 rct2.showVal();// 输出 40, 10 Rectangle rct3 = (++rct1); rct1.showVal();// 输出 42, 12 rct2.showVal();// 输出 40, 10 rct3.showVal();// 输出 42, 12 system("pause"); return 0;}
++i 是自身先自加一,后返回自身
i++ 是先拷贝自身,再自加一,返回自身的拷贝
因此在结果相同的情况下,应优先考虑使用 ++i,例如for循环
public限定符所修饰的成员变量和函数可以被类的函数、子类函数,以及类对象访问
protected限定符修饰的成员变量和函数可以被类的函数访问,子类函数访问,但是不能被类对象所访问
private限定符修饰的成员变量和函数只能被类函数访问,子类函数无法访问
根据访问权限总结出不同的访问类型,如下所示:
访问 | public | protected | private |
---|---|---|---|
基类 | YES | YES | YES |
派生类 | YES | YES | NO |
外部类 | YES | NO | NO |
继承类型
继承类型 | public | protected | private |
---|---|---|---|
公有继承 | public | protected | private |
保护继承 | protected | protected | private |
私有继承 | private | private | private |
#includeclass Shape{public: void setWidth(int w){ width_ = w; } void setHeight(int h){ height_ = h; }protected: int width_; int height_;};// 派生类class Rectangle : public Shape{public: int getArea(){ return (width_ * height_); }};int main(void){ Rectangle rect; rect.setWidth(5); rect.setHeight(7); // 输出对象的面积 std::cout << "Total area: " << rect.getArea() << std::endl; system("pause"); return 0;}
扩展:在类外访问类中私有成员变量
定义围墙类,使用指针暴力修改类中私有成员的值,实现翻墙代码如下
#includeclass Wall{public: void setA(int a){ a_ = a; } void print(){ std::cout << a_ << std::endl; std::cout << b_ << std::endl; }private: int a_ = 10; int b_ = 100;};int main(){ Wall w1; //w1 为8个字节 std::cout << sizeof(w1) << std::endl; //把 w1的地址赋值给 Wall指针 w2 Wall* w2 = &w1; //获取到变量b_的地址 int* w3 = ((int *)w2 + 1); //修改变量b_的值 *w3 = 0; w1.print(); system("pause"); return 0;}
从下图打印结果,可以看出我们已经通过指针暴力修改了围墙类中私有成员变量b_
的值
结论:访问限定符在编译时起作用,当数据映射到内存后,没有任何访问限定符上的区别
要求:virtual的用法以及使用场景,代码实现
通过构建虚函数实现多态,使类函数的调用不是在编译期被确定,而是在运行时被确定
class Base{public: virtual void print(){ std::cout << "Base"; }};class Derived :public Base{public: void print(){ std::cout << "Derived"; }};
通过构建纯虚函数,实现抽象类,用于提供模式及后代类应遵循的原则
class Base{public: //类中只要有一个纯虚函数就称为抽象类 //抽象类无法实例化对象 //子类必须重写父类中的纯虚函数,否则也属于抽象类 virtual void func() = 0;};
【普通继承】
class Base{private: int a; int b;public: void CommonFunction(); //void virtual VirtualFunction();};class DerivedClass : public Base{private: int c;public: void CommonFunction(); //void virtual VirtualFunction(); //void virtual VirtualFunction2();};
下图是VS2013如上代码编译的结果,从图可知普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。而子类的排布方式是先是排布了从父类继承来的的成员变量,接着排布子类的成员变量,成员函数不占字节
给基类加上虚函数,注释子类DerivedClass,这时的内存排布如下图:内存结构被分成了两个部分,上面是内存分布,下面是虚表。VS编译器把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面是虚表,在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。编译器是在构造函数创建这个虚表指针以及虚表的
把子类的注释去掉后,编译发现结果和预期的一样,因为有多个虚函数,所以序号1的虚函数在虚表中被列了出来,它是子类特有的
【多重继承】
多继承即一个子类可以有多个父类,它继承了多个父类的特性
class Base{private: int a; int b;public: void CommonFunction(); void virtual VirtualFunction();};class DerivedClass1 : public Base{private: int c;public: void CommonFunction(); void virtual VirtualFunction();};class DerivedClass2 : public Base{private: int d;public: void CommonFunction(); void virtual VirtualFunction();};class DerivedClass : public DerivedClass1, public DerivedClass2{private: int e;public: void CommonFunction(); void virtual VirtualFunction();};
其它类的内存分布上面已经讨论过了,这个多重继承的类DerivedDerivedClass内存分布和其它不一样。由上向下,并列排布两个继承的父类DerivedClass1与DerivedClass2,还有自身的成员变量e
DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;DerivedClass2的内存排布类似DerivedClass1,DerivedClass2里也有一份Base
【虚继承】
虚继承作用减少过多冗余代码,防止产生二义性
class Base{private: int a; int b;public: void CommonFunction(); void virtual VirtualFunction();};class DerivedClass : virtual public Base{private: int c;public: void CommonFunction(); void virtual VirtualFunction();};
原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr,另一个是vfptr。vbptr是这个DerivedClass对应的虚表指针,它指向DerivedClass的虚表vbtable,另一个vfptr是虚基类表对应的虚指针,它指向vftable
因此虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担
总结(当基类有虚函数时):
每个类都有虚指针和虚表;
如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)
有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;
如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份
扩展:编译器是如何利用虚表指针与虚表来实现多态的?
当创建一个含有虚函数的父类对象时,编译器在对象构造时将虚表指针指向父类的虚函数,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(虚表里面的虚函数入口地址是子类的)
调用Base *p = new Derived(); 生成的是子类的对象,在构造时,子类对象的虚指针指向子类的虚表,由Derived到Base的转换并没有改变虚表指针,所以p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态
将圆类作为基类,公有派生出圆柱体类和球类
圆柱体类:
数据成员height,表示高 定义无参构造,半径默认值为1,高度默认值为1 定义有参构造,按指定值创建圆柱高度的设置函数和获取函数 求表面积函数 getArea 求体积函数 getVolume
球类:
定义有参构造,按指定值创建球 求表面积函数 getArea 求体积函数 getVolume
编写测试主函数,创建圆柱体类对象和球类对象,分别输出其表面积和体积
class Cylinder:Circle{public: Cylinder(){ radius_ = 1.0; height_ = 2.0; } Cylinder(double r, double h){ radius_ = r; height_ = h; } double getHeight() const{ return height_; } void setHeight(const double h){ height_ = h; } //求表面积 double getArea() const{ return 2 * M_PI*radius_*radius_ + 2 * M_PI*radius_*height_; } //求体积 double getVolume() const{ return M_PI*radius_*radius_*height_; }private: double height_; double radius_;};class Sphere :Circle{public: explicit Sphere(double r){ radius = r; } //求表面积 double getArea() const{ return 4 * M_PI*radius*radius; } //求体积 double getVolume() const{ return 4 * M_PI*radius*radius*radius / 3; }private: double radius;};int main() { Cylinder c1; Cylinder c2(2.0, 2.0); double c1_area = c1.getArea(); double c1_volume = c1.getVolume(); std::cout << "c1的表面积为:" << c1_area << std::endl; std::cout << "c1的体积为:" << c1_volume << std::endl; double c2_area = c2.getArea(); double c2_volume = c2.getVolume(); std::cout << "c2的表面积为:" << c2_area << std::endl; std::cout << "c2的体积为:" << c2_volume << std::endl; Sphere sphere(1.0); double sphere_area = sphere.getArea(); double sphere_volume = sphere.getVolume(); std::cout << "sphere的表面积为:" << sphere_area << std::endl; std::cout << "sphere的体积为:" << sphere_volume << std::endl; system("pause"); return 0;}
转载地址:http://zzozi.baihongyu.com/