C++学习笔记(更新中)
c和c++的区别
a. c是结构化的语言,面向过程,重点在于数据结构和算法
b. c语言的api比较简洁
c. c++包含了绝大部分c语言的功能,并且提供oop(面向对象)和gp(类属编程)
d. c++有更严格的类型检查、大量额外的语言特性(rtti,异常)
e. c++也比较简洁,有运算符重载,隐式转换,
f. c语言的struct不能声明函数,没有模板,异常,继承
c++中四个与类型转换相关的关键字,比较他们
a. /最常用的类型转换符,在正常状况下的类型转换/
static_cast(varible)
/用于取出const属性,把const类型的指针变为非const类型的指针/
const_cast(varible)
c. /*dynamic_cast 主要用于执行“安全的向下转型,在程序运行期间判断\
但只在源类型具有多态类型时合法,即该类至少具有一个虚拟方法。*/
dynamic_cast(varible)rtti
i. 通常用于向下转型,父类转换为子类(可以调用父类和子类共有的对象,调用子类独有的会错误),其中父类必须 要有虚函数(多态类型),否则错误
iv. /*interpret是解释的意思,reinterpret即为重新解释,此标识符的\
reinterpret_cast(varible)
优先级队列,less greater
排序算法
a. 稳定的只有:插入,冒泡,归并(空间为o(n)),基数排序
b. 基数排序是先按个位排序,然后十位,依次到最高位时间复杂度和空间复杂度o(d(n+r)),d是基数,r是位数
c. 插入排序在最优的情况下只需o(n).
d. 快速排序的空间复杂度是o(logn),因为快速排序是需要返回一个结果值,需要o(logn)个结果值。
大端和小端
a. little-endian
i. 对字节:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
ii. 对位域:按照结构体从上往下,分配到内存的从低到高
b. big-endian
i. 对字节:就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
ii. 同小端相反
c. 比如0x1234,34是低位字节,12是高位字节
位域
a. 一个位域必须存储在同一个字节中,不能跨两个字节,故位域的长度不能大于一个字节的长度
b. 位域的对齐
3.如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,vc6采取不压缩方式,gcc和dev-c++都采用压缩方式; 整个结构体的总大小为最宽基本类型成员大小的整数倍
如果位域字段之间穿插着非位域字段,则不进行压缩;(不针对所有的编译器)
int x = 1,int y = ~x;则 y 为 -2
在计算机中整数的真值用补码形式表示,正数的补码是它本身,负数的补码是原数值除符号位按位取反再加一,由补码求原数值也是按位取反再加一,那么1111 1110 除符号位按位取反再加一变成 1000 0010,即 -2。又如0x80000000代表最小的整数
volatile关键字是一种类型修饰符
a. 用它声明的类型变量表示可以被某些编译器未知的因素更改。
b. 用volatile关键字声明的变量i每一次被访问时,执行部件都会从i相应的内存单元中取出i的值。
c. 没有用volatile关键字声明的变量i在被访问的时候可能直接从cpu的寄存器中取值(因为之前i被访问过,也就是说之前就从内存中取出i的值保存到某个寄存器中),之所以直接从寄存器中取值,而不去内存中取值,是因为编译器优化代码的结果(访问cpu寄存器比访问ram快的多)。
以上两种情况的区别在于被编译成汇编代码之后,两者是不一样的。之所以这样做是因为变量i可能会经常变化,保证对特殊地址的稳定访问。
c++的深拷贝和浅拷贝
当在一个类中使用指针时,要特别注意拷贝构造函数的调用,如果不定义拷贝构造函数会执行浅拷贝,只是两个指针指向同一个地址,当析构的时候也许会出现重复释放一个地址,造成错误:
a. 拷贝构造函数的第一个参数必须是引用类型(不是引用类型会调用构造函数);
b. 拷贝初始化(用 = 号来初始化时)通常使用拷贝构造函数来完成(myclass my1 = my2)
c. 还在下列情况下使用拷贝构造函数
i. 讲一个对象作为实参传递给一个非引用类型的形参
ii. 从一个返回类型为非引用类型的函数返回一个对象
iii. 一个对象需要通过另一个对象初始化
d. 深拷贝需要在函数中为被初始化的类成员分配空间,而不是简单的让指针指向同一个内存地址(浅拷贝)
j. c++的父类指针可以指向派生类的对象,如果调用的是虚函数,则在运行时会动态绑定到子类的函数。
如果不是虚函数,只能调用父类中的成员,若调用子类的成员,则出错
a. 一般情况下,对象注销时,先调用派生类的析构函数,然后调用基类的析构;
b. 当用new创建时,只调用基类的构造函数,例如:
parent *p = new child();
delete p;
则只调用基类的构造函数,
若基类的析构函数声明为virtual,则先调用派生类的析构,然后调用基类
为了防止下列这种情况:
parent *base;
child c;
base = &c;
如果不把基类的析构函数声明为virtual,则base销毁时只调用parent的析构函数,而不调用child的,会造成内存泄漏。
d. 当基类的析构函数声明为virtual,则后面所有子类的析构函数自动声明为virtual
e. 构造函数不能声明为虚函数
f. 虚函数会有额外的开销,因为要维护一个虚函数表
g. 析构函数也可以是内联函数
派生类和基类的继承需要梳理一下,很混乱
自己实现几个c语言的库函数,比如strcpy等 每日一个
使用sizeof()计算类大小的一些基本原则:
a. 类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑;
b. 类的总大小也遵守类似class字节对齐的(内存对齐需注意)
c. 成员函数都是不会被计算的;
d. 如果是子类,那么父类中的成员也会被计算;
e. 虚函数由于要维护虚函数表,所以要占据一个指针大小,也就是4字节(32位系统)。
this指针
a. 本质上是一个函数参数,只能在成员函数中使用,全局函数和静态函数不能使用
b. this在成员函数的开始前构造,在成员函数的结束后清除;
c. this指针不占用对象的空间
d. this指针的存放位置根据编译器的不同而不同
c++构造一个空类会产生四个函数
a. 构造函数 析构函数 拷贝构造函数 赋值函数
类的静态成员变量和成员函数
a. 类的静态成员变量使用时必须要先定义
b. 静态成员函数中不能调用非静态成员,其没有this指针(因为静态是在编译前就产生了内存)
c. 非静态成员函数中可以调用静态成员。因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的
d. 可以通过类名来调用静态成员和函数(因为在对象产生前就已经分配了内存)
类的const
a. 类中声明变量为const类型,但是不可以初始化
b. const常量的初始化必须在构造函数初始化列表中初始化,而不可以在构造函数函数体内初始化
如果将类类型对象定义为const,则其只能调用类中的const成员函数
多态
c++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。(重载不是多态) 那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。 最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用c++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数 只有指针或引用调用虚函数时才会执行动态绑定,如果是对象调用虚函数还是使用静态绑定。如果不想要执行动态绑定,可以通过在调用时增加作用域运算符 p->parent::fun();
组合是在新类中以原有类的对象作为数据成员,继承是在不改变现有的类的基础上,采用现有类的形式并在其中添加新代码,组合一般用于在新类中使用现有类的功能而不是他的接口的情况,就是新类用户看到的只是为新类所定义的接口。而继承则是用于在新类需要向基类转化的情况(多态),这也是组合和继承使用的最清晰的判断方法。
c++的继承
派生类中的基类成员也必须需通过基类的构造函数初始化(所以基类必须要有默认构造函数)i. 可以直接列表初始化调用基类的构造函数
ii. 否则会调用基类的默认构造函数初始化基类的成员 final,和override在行参列表之后,override可以防止由于派生类中虚函数的形参列表与基类的不同,而变成了重载不是覆盖,一旦如此会立即引发错误。final关键字可以保证之后任何尝试覆盖该函数的操作都将引发错误。 派生类向基类的转换只能是指针或引用(public继承才行),不存在对象之间的转换
i. 基类的拷贝构造函数,和赋值操作不是虚函数,传递一个派生类对象时,只会处理基类的部分,派生类的部分被切掉了,所以不能对象间转换
继承说明符:
i. 基类中protect成员不受继承说明符的影响
ii. 如果采用public继承,则基类成员的访问权限在派生类中不变
iii. protected继承,则基类的pubilc成员在派生类中会变成protected
iv. private,则基类的所有成员在派生类中都变成了private(除了protect成员)
引用的类型必须与其所应用的对象的类型一致(没有类型转换),常量的引用可以引用非常量,非常量的引用不能引用一个右值。 int & i = 5; //(error)
顶层和底层const
const表示指针本身是个常量(作为实参会被忽略),底层const表示指针所指的对象是个常量。函数形参使用const的引用可以保证函数能用于不能拷贝的类型;
//例如
const vector::iterator iter;
*iter = 10; //合法
iter++; //不合法
vector::const_iterator iter;
*iter = 10; //不合法
++iter; //合法
左值和右值:
? 当一个对象被用作右值的时候用的是对象的值(内容),当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)
? 左值表示有特定的名字的引用,右值没有。
? 左值可以当成右值来用,反之不行
? 返回左值引用的函数,连同赋值,下标,解引用,和前置递增递减运算符,都返回左值
? 返回非引用的函数,连通算术,关系,位,以及后置递增递减运算符,都返回右值
? 右值引用
? 通过&&来获得右值的引用
? 右值引用只能绑定到一个将要销毁的对象
c++继承体系中两种常用的关系:is-a(是一个) has-a(有一个)
c++的多态是允许子类类型的指针或引用,赋值给父类类型(可以通过多态调用子类的函数)
typedef typename 的使用
typedef 类型 定义名; typedef unsigned int uint;
类型说明只定义了一个数据类型的新名字而不是定义一种新的数据类型
typename关键字告诉了编译器把一个特殊的名字解释成一个类型,在下列情况下必须对一个name使用typename关键字:typedef typename cone::one_value_type two_value_type
1. 一个唯一的name(可以作为类型理解),它嵌套在另一个类型中的。
2. 依赖于一个模板参数,就是说:模板参数在某种程度上包含这个name。当模板参数使编译器在指认一个类型时产生了误解
尾置返回类型
auto fcn(t beg,t end) -> decltype(*beg)
c++中如果vector做成员变量,不可以在类中直接初始化如 vector v(10,0) 错误的这样
undef 就是取消宏的定义,然后可以用#define重新定义
define的宏定义是从定义的开始到文件的末尾,处理#define时应该忽略代码的逻辑
模板类是类模板实例化出来的。类模板中的成员函数全部都是模板函数
原来printf(“%d,%d\n”,pre a,pre b)是按照从右向左的顺序执行,尤其是自增自减运算要注意
静态变量是存放在全局数据区(初始化数据段或未初始化数据段),sizeof是计算栈中分配的大小,所以不会计算在内
不能给指针分配一个任意的地址 int ptr; ptr = (int )0x8000; //不允许
括号中逗号运算符返回值是最后的表达式的值,例如:
func((1,2,3),(4,5)) 第一个括号返回3,第二个括号返回5
explicit关键字
在c++中,如果一个类有只有一个参数的构造函数,c++允许一种特殊的声明类变量的方式。在这种情况下,可以直接将一个对应于构造函数参数类型的数据直接赋值给类变量,编译器在编译时会自动进行类型转换,将对应于构造函数参数类型的数据转换为类的对象。如果在构造函数前加上explicit修饰词,则会禁止这种自动转换,在这种情况下,即使将对应于构造函数参数类型的数据直接赋值给类变量,编译器也会报错。只能使用直接初始化,而不能使用赋值初始化
volatile限定符
当对象的值可能在程序的控制或检测之外被改动时,应该将对象声明为volatile。告诉编译器不应对这样的对象进行优化。(比如程序有一个由系统时钟定时更新的变量)
虚函数表
带有虚函数的类中的每一个对象都有一个虚指针指向该类的虚函数表
c++为每个有虚函数的类创建一个虚函数表,虚函数表存在于类的内存中,是按照虚函数声明的顺序存储的(连续的)
如果子类覆盖了父类的虚函数,那么子类的虚函数指针会在虚函数表里覆盖父类的该虚函数的地址
当多重继承时,每个父类都有一个虚函数表。若无覆盖,子类的虚函数指针的地址放在第一个父类的虚函数表中
虚拟继承
为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题
在菱形继承中,直接继承会多拷贝一份基类,浪费空间
虚继承出现在多重继承中,。对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象,共享基类子对象称为虚基类
虚继承和直接继承的sizeof()计算
虚继承计算时需要加上父类的虚函数表指针(4个字节),父类的内存大小,自己的内存大小
直接继承时需要计算父类的成员变量内存大小(虚函数不算),自己的内存大小
内存计算时,类中多个虚函数只占用4个字节的虚函数指针
虚函数和普通函数入口地址的区别
每个虚函数在虚函数表中都占了一个表项,保存着它的入口地址。当一个包含虚函数的对象(不是对象的指针)被创建的时候,它在头部附加一个指针,指向虚函数表中相应的位置。调用虚函数时,不管是什么对象调用的,它都要先根据虚函数表查找函数入口地址,实现了“动态联编”,而不是像普通函数那样有个固定的入口地址。
c++的rtti(运行时类型识别)
typeid:返回指针或引用所指对象的实际类型。
i. typeid能够获取一个表达式的类型:typeid(e)。返回类型为type_info的类
ii. 如果操作数不是类类型或者是没有虚函数的类,则获取其静态类型;如果操作数是定义了虚函数的类类型,则计算运行时类型。
iii. typeid最常见的用途是比较两个表达式的类型,或者将表达式的类型与特定类型相比较。
dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用(基类必须要有虚函数)
转换成功返回1,失败返回0
c++编译器何时生成默认构造函数(只在被需要时才会调用)
默认函数有2种,1. 没有提供实参的构造函数;2. 提供了默认实参的构造函数 第一种是类成员中有成员是类对象,并且该成员的类含有默认构造函数,若无默认构造函数,则不生成; 基类带有默认构造函数的派生类。 带有虚函数的类(1、类本身带有虚函数;2、继承而来的虚函数),因为虚表指针vptr需要在默认构造函数中初始化(运行期间)带有虚基类的类(虚继承产生的)
c++为类重载赋值运算符时:
一定要防范对自我赋值的操作;(先分配,后析构) 大多数赋值运算符组合了析构和拷贝构造函数的功能;首先分配新内存(构造内容),然后释放就内存,重新赋值。
重入:
主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入os调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。如果有多个进程调用此函数就需要使用信号量来保证。满足下列条件的函数多数是不可重入的:
1) 函数体内使用了静态的数据结构;
2) 函数体内调用了malloc()或者free()函数;
3) 函数体内调用了标准i/o函数(标准i/o函数很多实现都以不可重入方式使用了全局数据结构)。
智能指针(1.防止内存泄漏;2.在多个对象间共享内存)
auto_ptr : 不支持引用计数,只能有一个指针享用一个对象。对对象的所有权会随着赋值转移,很容易出错。 shared_ptr : 允许多个指针指向同一个对象,而且可以在构造函数中指定自己的删除器(函数对象) weak_ptr :i. 弱共享,通常配合shared_ptr使用,不增加引用计数;
ii. 可以使用lock()函数判断其所指向的shared_ptr对象是否存在,存在的话返回这个指针,不存在返回一个空的shared_ptr指针。use_count()查看引用计数,reset()置空shared_ptr;
iii. 防止循环引用 unique_ptr:独占的指向一个对象
实现一个智能指针(手写的程度)
i. 注意几点,构造函数,拷贝构造函数,赋值运算符重载,析构时的引用计数变化
ii. 引用计数需要使用int *count;这样的话当多个指针共同指向一个对象时,引用计数在所有的指针中都是共享的
delete和delete[]的区别
原则上是 new和delete,new[]和delete[]对应 基本类型的对象没有析构函数,销毁基本类型的数组对象时调用delete和delete[]的结果一样,都不会造成内存泄漏 对于自定义类型的数组销毁时,只能使用delete[](会调用说有数组成员的析构函数),而delete只会调用第一个的成员对于所有类型的单个对象,只能使用delete而不能使用delete[]。
使用静态函数可以加快运行速度(空间换时间)
经常需要调用的函数 无需实例化的线程安全的函数
怎么判断何时需要定义自己类的(析构,拷贝构造,拷贝赋值)
先判断是否需要自己的析构函数,如果需要,肯定也需要一个拷贝构造和拷贝赋值
栈中定义的变量就是局部变量,malloc从堆中申请的内存可以让栈中指针指向它;
void test()
{
char p=(char )malloc(100,1); // p为栈中的指针,指向堆中单元,这完全可以;
}
随着子程序的执行结束,栈中的局部变量将释放,p变量消失。则堆中这块变量
得不到释放,就会变成无主地,导致内存泄漏。所以,应主动先释放堆块。
防止内存泄漏
当获取资源的时候,就放进资源管理类(shared_ptr),通过析构函数自动释放。 shared_ptr和auto_ptr的析构函数都是使用delete,所以不应该用动态数组来使用,动态数组可以使用容器来取代。 auto_ptr只是指向一个对象,而且它管理的对象的所有权会转移,所以淘汰了·上一篇: 是幽默还是愚昧!
下一篇: 2018自媒体吸粉引流3大途径