30.C++复习篇
本章学习内容:
- 1.const
- 2.指针const
- 3.inline内联函数
- 4.函数重载
- 5.extern “c”
- 6.new/delete声明与释放
- 7.namespace命名空间
- 8.c++中的4种转换
- 9.拷贝构造函数
- 10.构造函数初始化列表
- 11.析构函数
- 12.const成员函数
- 13.const对象
- 14.栈、堆、静态存储区的区别
- 15.静态成员变量/静态成员函数
- 16.友元friend
- 17.operator操作符重载函数
- 18. 通过()操作符重载实现:函数对象
- 19. 操作符重载实现:类型转换函数
- 20.explicit显式调用(用来阻止隐式转换)
- 21.父类和子类中的同名成员/函数
- 22.子类对象初始化父类对象
- 23.父类对象初始化子类对象
- 24.纯虚函数vertual
- 25.泛型函数模板(兼容不同类型)
- 26.泛型类模板(兼容不同类型)
- 27.数值型函数模板和数值型类模板(兼容不同数值)
- 28.c++智能指针
- 29.qt中的智能指针
1.const
const和define宏区别
- const常量: 由编译器处理,它会对const常量进行类型检查和作用域检查
- define宏定义: 由预处理器处理,直接进行文本替换,不会进行各种检查
const在c++中为真正常量.示例:
const int c = 0; //const局部变量 int* p = (int*)&c; //会给p重新分配空间,而c还是处于常量符号表中 *p = 5; //此时修改的值是新的地址上,对于c而言,依旧为0 printf("c = %d,*p=%d\n", c,*p); //打印: c = 0, *p=5
2.指针const
1) 底层const(位于*左侧)
const int *p : const修饰*p为常量,也就是说该指针指向的对象内容是个常量,只能改变指向的地址.但是可以通过其他方式修改对象内容
例如:
int a=1,a=2; const int *p = &a; *p = 2; //error, 不能直接修改 a=2; //right,通过对象本身修改内容 *p = &b; //right,可以指向其它地址
2) 顶层const(位于*右侧)
int * const p : const修饰指针p是个常量,也就是说p指向的地址是个常量
例如:
int a=1,a=2; int * const p = &a; p = &b; //error,p指向的地址是常量,永远为a地址,不能修改
注意:顶层const变量可以替代mutable变量
3.inline内联函数
示例如下:
inline int max(int a, int b) { return a > b ? a : b ; }
- 普通函数:每次调用前,cpu都会保存现场(入栈),调用完后还要恢复现场(出栈)等额外开销.
- 内联函数:就会在每次调用的地方,将内联函数里的代码段”内联地”展开,所以省去了额外的开销
注意:当内联函数里的代码过多,且流程复杂时,编译器可能会拒绝该函数的内联请求,从而变成普通函数
4.函数重载
参数表不同主要有以下几种
- 1) 参数个数不同
- 2) 参数类型不同
- 3) 参数顺序不同
注意:
- 重载函数需要避免使用参数默认值
- 调用重载函数时,只会匹配函数参数表,与函数返回值无关
- 函数重载必须发生在同一个作用域中
- 重载函数的入口地址,不能直接通过函数名来获取
5.extern “c”
可以实现调用c库代码.
示例:
#ifdef __cplusplus extern "c" //通过c方式来编译add.h,也就是add()函数 { #include "add.h" } #endif
6.new/delete声明与释放
示例如下:
int *p = new int(); //默认值为0 int *p1= new int(1); //动态分配一个int空间给p1,并赋值为1 float *p2=new float(2.0f); //2.0后面加f,表示2.0是个float类型 int *p3 = new int; //默认值为随机值 string *p4 = new string[10]; delete p; delete p1; delete p2; delete p3; delete[] p4;
注意:
• 释放数组的空间时,必须使用delete[],而不是delete,避免内存泄漏
7.namespace命名空间
示例:
#include <stdio.h> namespace first //定义first命名空间 { int i = 0; } namespace second //定义second命名空间 { int i = 1;namespace internal //在second里,再次定义一个internal空间(实现嵌套) { struct position { int x; int y; }; } } int main() { using namespace first; //使用first整个命名空间,成为该main()的默认空间 using second::internal::position; //使用second->internal空间里的position结构体 printf("first::i = %d\n", i); printf("second::i = %d\n", second::i); position p = {2, 3}; printf("p.x = %d\n", p.x); printf("p.y = %d\n", p.y); return 0; }
输出结果:
first::i = 0 second::i = 1 p.x = 2 p.y = 3
8.c++中的4种转换
static_cast(静态类型转换)
用于变量和对象之间的转换,比如(bool,char,int等)
用于有继承关系的类对象指针转换,可以通过父类对象去初始化子类对象(注意只能初始化父类的那部分)
const_cast(去常类型转换)
常用于去除const类对象的只读属性
强制转换的类型必须是指针*或者引用&
示例-去除const对象的只读属性:
class test { public: int mval; test():mval(10) { } }; int main() { const test n1; //n1.mval = 100; //error,不能直接修改常量对象的成员 test *n2 = const_cast<test *>(&n1); //通过指针*转换 test &n3 = const_cast<test &>(n1); //通过引用&转换 n2->mval = 20; cout<<n1.mval<<endl; //打印20 n3.mval = 30; cout<<n1.mval<<endl; //打印30 }
dynamic_cast(动态类型转换)
只能用在有虚函数的类中,一般在多重继承下用的比较多,比如:
class basea { public: virtual void funca() { cout<<"basea: funca()"<<endl; } }; class baseb { public: virtual void funcb() { cout<<"baseb: funcb()"<<endl; } }; class derived : public basea,public baseb { }; int main() { derived d; basea *pa=&d; pa->funca(); //打印 basea: funca() /*通过强制转换执行*/ baseb *pb=(baseb *)pa; pb->funcb(); //还是打印basea: funca(), 因为pb还是指向pa,执行的还是pa的虚函数表 /*通过dynamic_cast执行*/ pb = dynamic_cast<baseb *>(pa); pb->funcb(); //打印 baseb: funcb() //编译器会去检测pa所在的地址,发现有多个虚函数表,然后根据 <baseb *>来修正指针pb return 0; }
reinterpret_cast(解读类型转换)
对要转换的数据重新进行解读,适用于所有指针的强制转换
9.拷贝构造函数
一般用于当类对象中有成员指针时,才会自己写拷贝构造函数,因为编译器自带的默认拷贝构造函数只支持浅拷贝
class test { //... ... test(const test& t) { //copy... ... } };
10.构造函数初始化列表
- 当类中有const成员变量时,则必须要用初始化列表进行初始化.
- 对于其它普通变量如果不初始化的话则为随机值.
- 初始化列表位于构造函数名右侧,以一个冒号开始,接着便是需要初始化的变量,以逗号隔开
示例如下:
class example { private: int i; float j; const int ci; int *p; public: test(): j(1.5),i(2),ci(10),p(new int(3)) //初始化i=2,j=1.5,ci=10 *p=3 { } };
11.析构函数
注意:
- 在类里,当定义了析构函数,编译器就不会提供默认的构造函数了,所以还要自己定义一个构造函数。
- 使用new创建的对象变量,在不使用时,需要使用delete,才能调用析构函数
构造函数的调用顺序
- 1. 首先判断父类是否存在,若存在则调用父类构造函数
- 2. 其次判断该对象的是否有类成员,若有则调用类成员的构造函数(调用顺序按照声明的顺序来构造)
- 3. 最后调用对象本身的构造函数
12.const成员函数
- cosnt成员函数里只能调用const成员函数
- const成员函数中不能直接修改成员变量的值
- 只有对mutable成员变量或者顶层const成员是可以修改的
- 如果用const修饰的函数,那么该函数一定是类的成员函数
13.const对象
- const对象的成员变量不允许被改变,
- const对象只能调用const成员函数,而非const对象可以访问const成员函数
- const对象是编译阶段的概念,运行时无效
- const对象可以通过const_cast强制转换来实现改变其中成员变量的值
14.栈、堆、静态存储区的区别
栈
用来存放函数里的局部变量,当调用某个函数时(执行某个代码段),会将该函数的变量(从数据段读出)入栈,然后退出函数的时候,会将该局部变量出栈进行销毁.
一般如果局部变量未初始化的话,都是随机值
堆
堆由程序员分配释放new/delete,所以需要注意内存泄漏问题
一般new分配的对象变量,其成员都是随机值
静态存储区
用来存放全局变量,一直会存在的,一般编译器为自动将未赋值的全局变量进行一次清0
15.静态成员变量/静态成员函数
- 在类里定义时直接通过static关键字修饰
- 静态成员变量需要在类外单独分配空间,而静态成员函数则不需要
- 静态成员变量在程序内部位于静态存储区
- 对于public公有的静态成员变量/函数时,可以直接通过类名进行直接访问
- 静态成员函数中不能访问非静态成员变量,因为它属于整个类的,没有隐含this指针
示例如下:
class test{ private: static int mval; public: test() { print(); } static int print() //静态成员函数是存在代码段中,所以不在类外定义也可以 { cout<<"mval="<<mval<<endl; } };
int test::mval=4; //静态成员变量存在静态存储区中,所以需要在类外定义 int main() { test::print(); //通过类名直接访问静态成员函数,打印: mval=4 }
16.友元friend
- 友元的好处在于,方便快捷.可以通过friend函数来访问这个类的私有成员
- 友元的坏处在于,破坏了面向对象的封装性,在现代已经逐渐被遗弃
示例:
#include "stdio.h" class test{ private: static int n; int x; int y; public: test(int x,int y) { this->x = x; this->y = y; } friend void f_func(const test& t); //声明test的友元是f_func()函数 }; int test::n = 3; void f_func(const test& t) { printf("t.x=%d\n",t.x); printf("t.y=%d\n",t.y); printf("t.n=%d\n",t.n); //访问私有静态成员变量 } int main() { test t1(1,2); f_func(t1); return 0; }
17.operator操作符重载函数
使'+,-,*,/'等操作符拥有了重载能力,能够实现对象之间的操作,而不再单纯的普通变量之间的操作了.
示例如下,实现一个加法类:
class add { double mval; public: explicit add(double t=0) { mval = t; } add& operator +(const add& t) //实现相同类对象相加 { this->mval += t.mval; cout<<"operator +(const add& t)"<<endl; return *this; //返回该对象,表示可以重复使用 }
add& operator +(int i) //实现int型对象相加 { this->mval += i; cout<<"operator +(int i)"<<endl; return *this; } add& operator +(double d) //实现double型对象相加 { this->mval += d; cout<<"operator +(double d)"<<endl; return *this; } add& operator = (const add& t) //重载赋值操作符 { cout<<"operator =(const add& t)"<<endl; if(this!=&t) { mval = t.mval; } return *this; } double val() { return mval; } };
int main() { add a1(11.5); add a2(1.25); a1=a1+a2; //相当于调用两步: a1.operator =(a1.operator +(a2)); cout<< a1.val() <<endl; }
运行打印:
18.通过()操作符重载实现:函数对象
- 函数对象是指该对象具备函数的行为
- 函数对象,是通过()调用操作符声明得到的,然后便能通过函数方式来调用该对象了.
- ()调用操作符可以定义不同参数的多个重载函数
- ()调用操作符只能通过类的成员函数重载(不能通过全局函数)
示例:
class test{ public: void operator () (void) //通过()重载操作符,来使对象具备函数的行为 { cout<<"hello"<<endl; } }; int main() { test t; t(); //来调用t这个函数对象打印"hello" }
ps:好处在于可以封装自己的成员以及其它函数,所以能够更好的面向对象.
19.操作符重载实现:类型转换函数
示例如下:
class test{ int mvalue; public: test(int i=0) { mvalue=i; } operator int() //重载int类型 { return mvalue; } }; int main() { test t(1000); int i=t; //等价于: i=t.operator int(); cout<<i<<endl; //i=1000 }
20.explicit显式调用(用来阻止隐式转换)
示例:
class test{ public: explicit test(unsigned int i) { cout<<"unsigned i= "<<i<<endl; } }; int main() { short num=3; //test t1=num; //error,因为explicit阻止short类型 转换为unsigned int 类型 /*只能有以下3个方法实现*/ test t2=(test)num; //c方式强制转换,不推荐 test t3=static_cast<test>(num); //c++方式强制转换 test t4(num); //手工调用构造函数 return 0; }
21.父类和子类中的同名成员/函数
- 子类可以定义父类中的同名成员和同名函数
- 子类中的成员变量和函数将会隐藏父类的同名成员变量和函数
- 父类中的同名成员变量和函数依然存在子类中
- 通过作用域分辨符(::)才可以访问父类中的同名成员变量和函数
示例1-通过子类访问父类同名函数和同名成员:
class parent{ public: int mval; parent():mval(100) { } void print() { cout<<"parent: mval="<<mval<<endl; } }; class child :public parent { public: int mval; child():mval(20) { } void print() { cout<<"child: mval="<<mval<<endl; } }; int main() { child c; c.parent::print(); //调用父类的同名成员函数 cout<<c.mval<<endl; cout<<c.parent::mval<<endl; //打印父类的同名成员变量 }
22.子类对象初始化父类对象
以上示例的parent类和child类为例,在编译器中,可以将子类对象退化为父类对象,从而实现子类来初始化父类,比如:
parent p1(child()); //child()构造函数会返回一个临时对象,从而通过子类初始化父类 child c; parent & p2 = c ; //定义p2是c对象的别名
23.父类对象初始化子类对象
只能使用static_cast或者c方式转换,以上示例的parent类和child类为例:
parent p; child *c = static_cast<child *>(&p);
24.纯虚函数vertual
- 在父类中用virtual声明的成员函数便为虚函数
- 虚函数的作用在于,能够正确调用某个同名函数是哪个类的对象
- 比如:当某个子类被强制转换为父类时,则父类的虚函数也会被替代为子类的,从而实现程序灵活性
一个典型的示例,如下所示:
class base //父类 { public: virtual void func() //声明func为虚函数 { cout<<"base: func()"<<endl; } }; class basea : public base //子类a { public: void func() { cout<<"basea: funca()"<<endl; } }; class baseb : public base //子类b { public: void func() { cout<<"baseb: funcb()"<<endl; } }; void print(class base& b) { b.func(); } int main() { basea ba; baseb bb; print(ba); print(bb); return 0; }
运行打印:
如上图可以看到,我们以print(ba)为例:
再调用print()函数时,会将basea ba转换为父类base,由于父类base有个func()虚函数,所以会被动态替换为ba子类的func()函数.所以会打印funca()
如果将上面代码virtual void func()改为void func()重新编译运行后,打印:
如上图可以看到,没有虚函数后,整个代码都变得没有灵活性,不适合类的扩展.
ps:在qt中,virtual用的非常多,比如qwidget的showevent函数:
virtual void showevent ( qshowevent * event );
假如我们需要在窗口显示时加点特效时,只需要重写它即可,而qt库只需要根据vertual特性来自动调用我们重写的函数,非常灵活.
25.泛型函数模板(兼容不同类型)
函数模板是c++中重要的代码复用方式, 可通过不同类型进行调用
- 通过template关键字来声明使用模板
- 通过typename关键字来定义模板类型
示例:
template <typename t> //声明使用模板,并定义t是一个模板类型 void swap(t& a, t& b) //紧接着使用t { t c = a; a = b; b = c; }
int main() { int a=0; int b=1; swap(a,b); //自动调用,编译器根据a和b的类型来推导 float c=0; float d=1; swap<float>(c,d); //显示调用,指定t是float类型 }
为什么函数模板能够执行不同的类型参数?
答:
- 其实编译器对函数模板进行了两次编译
- 第一次编译时,首先去检查函数模板本身有没有语法错误
- 第二次编译时,会去找调用函数模板的代码,然后通过代码的真正参数,来生成真正的函数。
- 所以函数模板,其实只是一个模具,当我们调用它时,编译器就会给我们生成真正的函数.
函数模板也支持多参数,示例如下(如果定义了返回值模板,则必须要显示指定返回值类型,因为编译器不知道到底返回什么类型):
#include <iostream> using namespace std; template<typename t1,typename t2,typename t3> t1 add(t2 a,t3 b) { return static_cast<t1>(a+b); } int main() { // int a = add(1,1.5); //该行编译出错,没有指定返回值类型 int a = add<int>(1,1.5); //指定t1为int形 cout<<a<<endl; //打印2 float b = add<float,int,float>(1,1.5); //指定t1,t2,t3类型 cout<<b<<endl; //2.5 return 0; }
26.泛型类模板(兼容不同类型)
类模板和函数模板一样,都是进行2次编译,需要注意的是定义对象必须显示指定所有类型
示例:
template<typename t1,typename t2,typename t3> class operator{ public: t1 add(t2 num1,t3 num2) { return num1+num2; } };
int main() { operator<float,int,float>t; cout<<t.add(11,11.5)<<endl; //11+11.5 = 22.5 return 0; }
27.数值型函数模板和数值型类模板(兼容不同数值)
数值型和泛型类似,但是数值型模板必须在编译时被唯一确定
示例1-数值型函数模板:
template <typename t,int n > //定义一个泛型值t,还有个int型的数值 void func() { t arr[n]; //使用模板参数t和n定义局部数组 } int main() { func<int,10>(); //相当于实现 int arr[10] }
示例2-数值型类模板(实现1+2+3+....+n值):
template < int n > class sum { public: static const int value = sum<n-1>::value + n; //通过sum<n-1>::value实现递归调用,并返回该临时对象 }; template < > //完全特化,因为我们知道n为1,所以不需要写< int n > class sum < 1 > //重载sum类(类似于函数重载),当n==1时调用该类 { public: static const int value = 1; }; int main() { cout << "1 + 2 + 3 + ... + 10 = " << sum<10>::value << endl; cout << "1 + 2 + 3 + ... + 100 = " << sum<100>::value << endl; return 0; }
28.c++智能指针
头文件<memory>
1)auto_ptr
- 生命周期结束时,自动摧毁指向的内存空间
- 不能指向堆数组(因为auto_ptr的析构函数删除指针用的是delete,而不是delete[])
- auto_ptr的构造函数为explicit类型,所以只能显示初始化
- 提供get()成员函数,可以用来查看类里的指针地址
- 一个堆空间永远只属于一个对象(比如auto_ptr被拷贝/赋值,则自身的指针指向的地址会被抢占)
示例如下:
#include <iostream> #include <memory> using namespace std; class test{ int mvalue; public: test(int i=0) { mvalue = i ; cout<< "test("<<mvalue<<")"<<endl; } ~test() { cout<< "~test("<<mvalue<<")"<<endl; } }; int main() { auto_ptr<test> p1(new test(1)); auto_ptr<test> p2(new test(2)); cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p2 = p1; cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; return 0; }
运行打印:
如上图所示,当我们执行p2=p1后,便执行了p2的析构函数进行自动释放了.并且p1.get()=0,所以auto_ptr具备自动释放功能以及同块堆空间下只能有一个指针对象特性
2) shared_ptr (需要c++11支持)
- 带有引用计数机制,支持多个指针对象指向同一片内存(实现共享)
- 提供swap()成员函数,用来交换两个相同类型的对象指针地址
- 提供unique()成员函数, 判断该指针对象地址是否被其它指针对象引用
- 提供get()成员函数,用来获取指针对象指向的地址
- 提供reset()成员函数,将自身指针对象地址设为null,并将引用计数-1(当计数为0,会自动去delete内存)
- 提供use_count()成员函数,可以用来查看引用计数个数
示例如下所示:
class test{ public: int mvalue; test(int i=0) { mvalue = i ; cout<< "test("<<mvalue<<")"<<endl; } ~test() { cout<< "~test("<<mvalue<<")"<<endl; } }; int main() { shared_ptr<test> p1(new test(1)); shared_ptr<test> p2(new test(2)); cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p1.swap(p2); //互换p1和p2指针指向的地址 cout<<"p1: addr="<<p1.get()<<endl; cout<<"p2: addr="<<p2.get()<<endl; p1 = p2; //使p1指向p2指向的地址,并且释放p1之前指向的地址 cout<<"p1:addr="<<p1.get()<<", p2:addr="<<p2.get()<<endl; cout<<"p1: unique="<<p1.unique()<<endl; //p1和p2指向同一片内存,所以为0 cout<<"p1: count="<<p1.use_count()<<endl; return 0; }
运行打印:
29.qt中的智能指针
-qpointer
- 当其指向的对象被销毁时,本身会自动赋值为null(从而避免被多次释放和野指针)
- 缺点在于,该模板类析构时,不会自动摧毁所指向的对象(需要手工delete)
-qsharedpointer
- 带有引用计数机制,支持多个指针对象指向同一片内存(实现共享)
- 可以被*地拷贝和赋值
- 当引用计数为0(最后一个指针被摧毁)时,才删除指向的对象(和shared_ptr类似)
-qscopedpointer
- 优点在于生命期结束后会自动删除它所指的对象(不需要手工delete)
- 不支持多个qscopedpointer指针对象指向同一片内存(不能共享)
示例:
qscopedpointer<qpushbutton> p1(new qpushbutton);
上一篇: .net图表之ECharts随笔07-自定义系列(多边形)
下一篇: 你快跑啊