预处理器编译指令
#include "xx.h"
#include <iostream>
编译的时候这些会被头文件替换掉,c++会自动搜索;引号从当前工程开始搜索
#define INT_MAX 5
c风格定义常量,c++替代方式为
const int a = 5;
常量定义方式必须是马上初始化,定义不初始化编译器会报错。
#define XXX x+y
对应的c++方式为(c风格是直接替换,c++是计算完了才替换) 这里有问题。。。。。。。。。。。。。。。typedef x+y XXX
名称空间
c++即使引入了头文件,还必须显示声明头文件所属的名称空间
其实头文件中类变量模板结构体等声明都引入了,按照道理来说应该可以识别直接用了,但c++还是强制必须显示声明所属名称空间或者显示声明名称空间中所需要用的对象等,这点还是违反常识的。这点java中import指令比较好。
引入的方法有三种:
using namespace std;
using std::cout;
std::cout<<"第三种方式";
定义名称空间的方式:
namespace xxx{}//注意最后不加分号
在哪里声明名称空间就会在哪里引入对应的变量或者定义,其生命周期由声明所属位置决定,注意不要重复定义变量,不同的是using编译指令全部引入,重新定义同名的会覆盖引入的变量,而using声明不会,编译器会报错。
杂七杂八
int main(){}//唯一符合标准的声明方式,return 0 可以省略
sizeof(int); sizeof 5; sizeof(5); //sizeof是运算符,不是函数
变量名不该用下划线或者双下划线开头,这些都留给实现,不然可能会导致意外发生。
整型包含bool、char、short、int、long、long long,除了bool外都有unsigned和signed版本,整型字面量表示有18、015、0x23分别对应着10、8、16进制,数字默认为int,除非int存不下。bool型可以与数字自由转换,赋值的时候可以赋true/false
'\0' //数字加反斜杠也表示转移,典型的就是字符串结尾
c++不会检测数组越界,实际上指针可以随便访问内存位置
引用变量声明的时候必须初始化,这个和指针不同
const int* p = &a;
int* const p = &a;
void fun(int(*a)[4])
void fun(int a[][4])
#ifndef #define #endif 中一般包含常量、类原型、函数声明、结构定义等
volatile取变量值的时候不要去寄存器中取,直接去内存取,变量值是易变的
mutable 用于定义结构体中,即使结构体是常量,包含的这个基本类型还是可以修改的
struct a{
mutable int b;
};
函数声明、函数原型
函数声明类似于变量声明,给编译器使用,去除{}内定义部分只要原型就可以了,函数由返回值、函数名、参数列表决定,函数名的类型由返回值和参数列表决定(重载是在函数名相同的情况下参数列表不同,和这个函数名所代表的类型这个概念一点关系都没有),因此将函数名替换成*p,p就是函数指针,用函数指针调用函数加不加解除引用符*都可以。赋值方式为:
p = funName//不要&
实际上函数名表示的就是存放函数片段的首地址
const double* (*pa[3])(int*)//有3个函数指针元素的数组
当类重载了()时,生成的对象也可以当成函数用,也就是说函数可以看成一个对象,并且这个对象重载了()运算符,这个在stl实现中被大量使用,类似于javascript中的函数参数列表中可以传入函数名,不同的是c++中传入的是重载了()运算符的对象,然后将这个对象当函数使用。
输入输出流
不要把cin、cout看成普通对象,他们对应的是输入输出流;
cin、cout都是遇到回车才开始读入或者读出刷新缓冲区流,当然检测到需要用户输入的时候也会刷新,输入输出流缓冲区都会刷新
ios_bas---->ios---->{1 istream 2 ostream}---->iostream; 引入iostream就等于引入了cin、cout、clog、cerr对象和对应的宽字符版本
cin
对于cin来说,空白是特殊的概念,对于
int a;cin>> a;
这种表达,空白用于区分是否是两个输入;
cin如果读入一个整数时,会从输入流开始读取合法的字符直到遇到非法字符(比如-123z,z会留在缓冲区)或者遇到空白为止,注意的是由于回车肯定存在(打了回车才会刷新输入流缓冲区开始输入),这个回车会留在缓冲区,如果下面读取字符的话会什么都读不到。
将普通空白(不包括回车)当然字符的方法是
cin.getline(char*,int);
cin.get(char*,int)
读取长度减1的字符(最后给'\0'),多的会留在输入流中或者遇到‘\n’,两者区别是getline会扔掉最后的‘\n’,而get最后的'\n'还是留在输入流中。
get还有重载版本,这时候回车也会当成普通字符读取,包括
istream& get(char&);
char get();
第一个更好用,可以连接,而且当输入的字符与char不匹配的时候(比如文件尾EOF),不会赋值给char&,而第二个还是会尝试赋值给char(虽然是手动控制的)会出错,解决方法是返回值赋值给int;
所有遇到非法输入时候,比如要输入数字但遇到的是字符,getline、get超过长度自动结尾,读取遇到文件尾,get、getline直接读到回车符的时候,都会设置状态位表示出错,状态位有eofbit、failbit、badbit,对应的函数或常量有fail()、eof()、EOF,这些都用clear()清除,但是clear()函数只是清除状态位复原输入输出流,流中多余的东西还是要程序另外去除。
cout
cout.fill(char); 当设置字符宽度较宽多余的一般用空白表示,这个函数可以用其他字符填充;
cout<<setw(int);
cout.width(int); //设置宽度,下次输出完成就会消失
cout.precision();
cout<<setprecision()<<;//设置精度,一直存活
cout<<hex<<oct<<dec;
文件输入输出流
istream类派生出ifstream,ostream派生出ofstream
ifstream fin;
fin.open(FILEPATH); //open好像只能用默认参数
ifstream fin(FILEPATH);
ifstream fin(FILEPATH,ios_base::in);
fin对象使用的时候也用>>运算符,对比cin不用\n才触发缓流,但同样的对于空白都是当成特殊字符处理.
读到文件尾也用eofbit、eof()、EOF常量判断
while(fin.get(char&)){}
int ch;
while((ch=fin.get())!=EOF){}
fin.is_open(); // fin.good()
fin.close();
ofstream fout(FILEPATH,ios_base::out|iso_base::trunc);//默认行为
ofstream fout(FILEPATH,ios_base:out|ios_base::app);
fstream finout(FILEPATH,ios_base::in|ios_base::out|ios_base::app);
fout<<4; //然后就可以用<<运算符
二进制形式输入输出
ifstream fin(FILEPATH,ios_base::in|ios_base::binary);
fin.read((char*)&p1,sizeof(p1));//假设p1是结构体变量
ofstream fout(FILEPATH,ios_base::out|ios_base::app|ios_base::binary);
fout.write((char*)&p1,sizeof(p1));
控制移动文件位置
ifstream fin(FILEPATH,iso_base::in);
fin.seekg(int,ios_base::cur);//int为偏移量,还有其他参数ios_base::beg, ios_base::end
fin.seekg(int);// 绝对位置
fin.tellg();//返回当前位置,类型为streampos
ofstream fout(FILEPATH,ios_base::out|ios_base::app);
fout.seekp(int,ios::base::beg);
fout.seekp(int);//绝对位置
fout.tellg();
初始化
首先初始化应该和赋值区分开,特别对于对象,对象新创建的时候顺便初始化一定会调用拷贝构造函数,比如
A a1 = a2;// a2是已经创建的对象
而对象创建后叫做赋值,会调用赋值重载函数,比如
A a1;
a1 = a2;
c++有各种版本的初始化:
int a = 4;
int a(4);
int a{};
int a{3};
int a={3};
{}版本的初始化不允许缩窄,比如double赋给float不允许;而普通赋值或者初始化数字之间是可以随意相互赋值的。
数组的初始化:
int a[3] = {1};
int a[3] = {};
int a[3]{}; //c++11
int a[3]{1};//c++11
比较有用的初始化方式:
int a[3] = {0}; //定义一个数组全部赋值0
char a[] = "adfadf"; //自动确定长度,注意"s"表示's''\0'和's'不是一回事
结构的初始化方式和数组类似,不明确赋值的部分都是0
struct A{};
A a[3] = {
{},
{}
};
类的列表初始化:
class A
{
private:
int a;
const double b;
public:
A():a(2),b(2.13){}
};
和在构造函数中赋值不同的是在括号内表示先申请变量,然后赋值,而列表表示申请变量空间后直接初始化,因此const double没有语法错误,在括号内赋值很明显是错的。
在c++11中可以像java一样初始化
class A
{
private:
int a = 1;
const double b = 3.2;
public:
A(){}
};
静态变量的初始化
class A
{
private:
static int a;
};
int A::a = 4;//在实现中的初始化方式
静态变量的初始化也是反常识的,明明类定义中已经指明了a的类型,实现中还要强调a的类型,这个和auto一对比,感觉就是编译器的职责范围和编码人的职责范围有点错乱
类型转换
double b = 2.42;
int(b);
(int)b;
static_cast<int>(b);
auto x = static_cast<int>(b); //static_cast其中<>和()其中一个能转化成另一个就是合法的
其中auto表示自动推断变量类型,但个人觉得这个不好,程序员应该很清楚变量类型,因为你要用这个变量去操作后面的程序行为, 比如auto的应用之一是stl模板中的迭代器避免了冗长的vector<int>::iterator it = v.begin()可以用auto it = v.begin()-------c++11才可以使用, 但这种为嘛不像java一样定义一个iterator的接口或者抽象类抽出共同的行为??违反常识的行为。
dynamic_cast<Type*>(p)类似于java中的instanceof,判断父类是否可以转换成子类,java中的instanceof被喷成什么样这个RTTI运算符就被喷成什么样。行为是如果可以转换就返回对应的指针,不能转换就返回空指针;类似的还有dynamic_cast<Type&>(p),不同的是如果不能转换就会产生异常要用try-catch捕获, 这种同样的符号行为不一致又增加了学习成本
类似的还有typeid( 类名)==typeid(*指针),,当然指针是空指针会异常,所以正确的行为应该是
if(!p&&typeid(ClassName)==typeid(*p))
typeid返回type_info类
static_cast什么都能转化,dynamic_cast好像只用于指针或者引用的转化
自定义对象和内置类型的直接转换
内置类型转化为对象:
A a = 5;// A 为自定义的类
只要A中定义了单参数的构造函数,5可以转化为这个单参数的类型,就会隐式调用这个构造函数生成对象,效果就是好像直接将int型的5隐式转为了A类型。去除这种隐私转换的方式为使用explicit限制构造函数的这种行为:
class A
{
private:
int a;
public:
explicit A(int a){this->a = a;}
};
但显式转换还是可以的
A a = (A)4;
隐式转化在函数参数值传递、返回值类型不匹配的时候编译器都会自动尝试转化,不管是内置类型之间还是内置类型和对象之间,但如果对于重载函数,这种不匹配可能会带来二义性
这种转化也不与重载operator=()函数冲突,当初始化的时候,必然调用的是单参数的构造函数,当赋值的时候,会尝试调用operator=()函数,如果匹配会调用operator=(),但不匹配会调用单参数的构造函数,然后调用必然存在的 A& operator=(const A&)版本,但如果存在符合版本的operator=()的时候,这两种方式都可以执行,因此这里其实有二义性,很奇怪的行为!!!!!
对象转换为内置类型可以通过转换函数实现,转换函数一定是成员函数没有返回值没有参数列表,operator int(){},但转换函数很容易引起二义性,比如
A a = 8;
a+4;//这里的a+4是将a为int型然后+4还是将4转为A型然后调用A的operator+()方法?
枚举
枚举就是用来定义一组常量
enum a{b,c,d,e,f};//内部从0开始
其实就是整型变量,可以转成整型。另一种应用就是在类中定义一个常量
class A
{
private:
enum {NUM = 100, XX =2};
}
字符串
最好用的显然是string类,getline友元函数,size()等同于length().
cstring定义中,strlen(char*)返回字符串长度但不包括'\0',strcpy(char*,char*)会把n2的值统统赋值给n1,因此可能越界;strncpy(char*,char*,int)拷贝n个字符,但如果n个都没有结束的话最后不会赋'\0'
const char* p = "adfsdfsd";
char *p = "dafdsafadf";
这两种定义字符串的方式都一样,都是字符串常量,不可以通过p去修改字符串的值。
string其实是 typedef basic_string<char> string
指针
int* p这种所谓的c++定义方式其实不必c的 int *p高明,强调不了p是int指针的作用,因为int* p1,p2中,p2还是整型的,不是指针。可以用
typedef int* intp;
代替,注意如果用
#define intp int*
这个是直接替代的,和未用这个指令没区别。
指针变量本身的长度是一样的,4个字节表示地址,内部是用整型表示的,但和int是两种类型,最典型就是指针加减法一下子指向下一个对应的数据,而整型就是真的加减1.
int *p = new int[10];p内部肯定是知道p指向数组的长度的,因此delete[] p的时候才能删除对应大小的内存,但这个长度不是公用的,因此p当成参数传入函数的时候,还要另外传一个长度。
delete释放已经释放过的内存是危险的,因为可能那块区域又赋值过了然后被释放了,但释放空指针没有问题,delete运算符内部应该自己判断的。
int a[10]; a表示a[0]的地址,因此int *p = a; &a表示整个数组的地址,因此int (*a)[10] = &a;注意与int *a[10]的区别,[]优先级大于*,因此这个表示为申请一个长度为10的数组,每个都是int*.
函数相关
内联函数没有外部链接性,因此必须定义必须和声明同文件,和模板类类似。
函数传参和返回值都可能发生隐式转换。
函数内部一般是临时变量,因此返回值一般不可以是引用或者指针,返回值如果是引用,一般是传入的参数也是引用,然后返回该参数;如果返回的是指针,除了返回传入指针外,还可以在函数内部通过new申请堆上内存,然后利用自动指针释放。
返回引用的示例:
const ostream& operator<<(const ostream& out,const A& a)
{
out<<a.member;
return out;
}
如果不加const的话,返回的引用在函数当成左值的时候值会被覆盖掉(但应该一般没人会这么将函数当成左值处理),函数返回引用当成左值的唯一运用就是重载operator[](),可以类似数组访问变量修改变量,注意返回值一定要是引用,并且2个const和非const两个版本。
引用参数和值传递参数对编译器来说是无法区分的,因此不能用于重载;参数列表为引用如果还是const的话能传递常量生成一个临时对象,该引用为临时对象的引用;如果不是const的话会报错。
const对于重载实在搞不清,各种版本,但是在类中,void fun() const 和void fun()肯定属于重载,类定义常量的时候只能用const版本,特别是对于operator[],一定要定义2个版本;但如果不在类作用域范围内,这两个函数不是属于重载。
自动指针:
会自动释放申请的堆上内存。
首先要#include <memory>,然后自动指针有三个,auto_ptr、unique_ptr、share_ptr,使用方式
auto_ptr<int> p(new int); // share_ptr、unique_ptr也是这么用
unique_ptr<int[]> p(new int[5]); //只能用unique_ptr
auto_ptr<int> p;
p = new double; //构造函数有explicit,因此这个是不可以的,必须显式转换
这三个指针的区别是auto_ptr类型智能指针指向的对象赋给其他指针时,当前指针会变成野指针,也就是说指向的对象的所有权没有了,因此在c++11这个被废除了。
unique_ptr也会释放所有权,只当临时右值的时候会释放,如果一直存在的话编译的时候就会报错,也就是说禁止释放所有权;share_ptr会通过一个计数器共同单元,当计数器为0时就会释放内存。
变量的作用范围
函数内部定义static,静态区分配变量,每次进入函数都是同一个。
int a;//所有文件都能访问到但是在其他文件中必须显示申明extern int a;
static int a;//文件内能访问到
局部变量会隐藏全部变量,但全部变量可以通过
::全局变量访问。
对于常量不一样:
const int i = 0;//文件内访问
const static int i = 0;//文件内访问
extern const int i = 0;//所有文件访问,其他文件中要显式声明extern int i
说是为了防止定义同名的常量。
函数定义默认为外部链接性;可以使用extern明指,也可以使用static 定义成文件内可见
类和对象
类定义中默认行为都是private,包括继承的时候。protected比private多了一个继承可见
类中定义内联函数的时候,要么直接放在类声明中,如果另外定义,那么一定要和类声明同文件,注意内联函数链接性只有文件内。
类成员变量如果有指针的话一定要深拷贝,除非不会赋值,但这个基本是不可能的。
类中可以嵌套其他类定义或者结构定义,符合类的作用域声明规则。
对于编译器来说,默认构造、复制构造、析构、赋值运算符如果没有覆盖版本编译器会自动添加,即使你写了重载版本编译器也会添加默认版本。
class A
{
private:
int* p;
public:
A(int a=0); // 单参数的构造函数可以用于隐式转化,explicit可以限制这种行为
A(const A& a);
virtual ~A();
A& operator=(const A& a);
friend ostream& operator<<(ostream& out,const A& a); //不定义友元一定是对象调用
};
A::A(int a){p = new int;*p = a;}
A::A(const A& a){p = new int;*p = *(a.p);}
A::~A(){delete p;}
A& A::operator=(const A& a)
{
if(this==&a)
return *this;
delete p;
p = new int;
*p = *(a.p);
return *this;
}
ostream& operator<<(ostream& out,const A& a)
{
out<<*(a.p);
return out;
}
继承的时候会子类会调用父类的默认构造函数默认构造函数,除非显示定义。
A::A():B(),a1(),a2(){} //A继承B的情况下
不显式定义拷贝构造的时候,父类的拷贝构造会调用基类的拷贝构造函数,但显式定义了父类的拷贝构造函数以后,如果不显式调用基类的拷贝构造函数会调用基类的默认构造函数,
默认行为明明可以识别基类的拷贝构造函数的,这种行为真的很奇怪。
赋值重载在父类不显示调用的情况下也会调用基类的赋值运算符,如果显式定义了,必须有这样的行为:
B& operator=(const B& b)//B继承A
{
....
A::operator=(b);//一定要显式调用
....
}
析构会自动调用,与创建顺序相反,注意的是要定义成virtual,防止基类指针指向派生类对象的时候释放内存不干净。
组合在c++还可以通过私有继承、保护继承实现(私有继承是继承的默认行为),但很明显没有组合好。私有继承和保护继承将原始公有方法都变成私有或者保护,派生类中还是使用的,但对外不可见。
构造函数的调用:
A a(参数列表);
A a = A(参数列表);
A a;//无参的构造调用不要加括号,不然就变成函数原型了,实际上加了括号变成了声明a是函数类型,编译器不会报错
A* a = new A;//无参构造
A* a = new A(参数列表);
运算符重载设计成成员函数可以少一元,带来额外的要求是对象总要是第一个元,设计成友元就没这个要求。operator<<肯定要定义成友元。
当对象定义成const 时候,只能调用
void fun() const;
这个版本,这个版本和
void fun();
属于重载,编译器是可以识别的。
当基类和派生类中都有同一友元函数时,派生类中调用基类的友元函数对象的类型一定要强制转成基类的,不然最匹配的就是派生类的友元函数变成了无限调用自身了。
多态
基类中函数定义为virtual,派生类中定义同名同参数列表的函数,然后基类引用或者指针指向派生类对象就可以构成多态。
注意:
- 派生类的同名同参数列表的函数即使前面不加virtual关键字还是属于多态函数,为了标识一般都加
- virtual并不是让基类和派生类拥有共同函数名+参数列表的唯一方法,实际上派生类中可以定义同名方法,通过类的作用域运算符区别。
- 如果定义的virtual函数名同但参数列表不同,会覆盖之前所有的virtual方法,但返回值可以由基类引用或者指针变成派生类的引用或者指针。
#include <iostream>
using namespace std;
class A
{
public:
virtual void print(int a){cout<<"A "<<endl;}
virtual ~A(){}
};
class B:public A
{
public:
virtual void print(int a,int b){cout<<"B"<<endl;}
virtual ~B(){}
};
int main()
{
A* a = new B;
a->print(2,3);
delete a;
cin.get();
}
这个编译会出错,A中的virtual方法被覆盖了,A中没有对应的print的方法也没法动态绑定。
virtual还能用于抽象基类(ABC),内部定义一个纯虚函数就可以了,纯虚函数的virtual不能省略,不然编译器会认为类未定义完全。
class A
{
public:
virtual void print(int a)=0;
virtual ~A(){}
};
纯虚函数有定义没定义好像都没用。即使有定义也没发生成具体的实例。
多重继承
多重继承的问题:1 两个父类来自同一祖父类孙子类中包含两个副本;2 两个父类中包含同名函数。
1不解决包含两个副本时:
Son* son = &grand;
grand中son两个副本,无法区分那个版本给son指针。
解决方法之一是显式指定
Son* son = (Father1*)&grand;
另一个解决方法是 Father 虚继承 Grand类,这样Grand只有一个副本
class Father1: virtual public Grand{}
但这样带来的问题是Son类调用构造函数对于唯一版本的Grand有两条路径调用Grand的构造函数,因此编译器直接调用默认版本,当然也可以显式指定:
Son():Father1(),Father2(),Grand(){}
子类对象直接调用父类继承到的同名函数很明显带有二义性,可以显式调用某个父类的方法
son->(Father1::print());
不这么做子类中可以显式定义一个同名方法,内部再调用某个父类的方法。
友元
友元函数:类中Friend定义函数
友元类: friend class B; 则在B类中可以使用这个声明所在类的所有成员
友元成员函数:friend void A::fun();要先声明A对象,不然无法识别fun函数;如果A对象用到了该对象的内部成员,则将实现放在该对象声明之后;如果用到整个对象,则声明
class B;
异常
throw 对象或者字符串给try-catch语句块,try捕捉catch得到对象,然后该怎么处理怎么处理。
模板
模板函数
template<class T>
void fun(T t1);
template<class T>
void fun(T t1,T t2); //模板也可以重载
模板只是告诉编译器如何创建函数,调用的时候才会创建具体的函数版本。
显式实例化其实告诉编译器马上生成一个版本的,意义不大,完全可以调用函数的时候生成。
template void fun<int>(int);
显式具体化可以对于模板再次定义自己的模板,但还不如定义同名的普通版本,语法这么复杂。
template <> void fun<int>(int){}
template<> void fun(int){}
优先级是普通函数>显式具体化>模板
调用方式:
fun(4);
fun<int>(4); //这种显然只能调用模板
注意模板的使用范围,特别是stl的运用,并不是所有类型都适用模板的。比如智能指针就不适合用于vector的类型参数,无法控制知能指针的行为。
模板类
template<class T = int> //可以有默认值,行为类似于默认参数列表
class A
{
public:
void fun();
};
template<class T>
void A<T>::fun(){}
template<class T,int n> //n是常量,不能修改
class A
{
public:
void fun();
};
template<class T,int n>
void A<T,n>::fun(){cout<<"a";}
template<> //显式具体化
class A<char*>
{
};
模板类声明和定义必须同文件,模板类也可以继承、组合等。继承或者组合后该类也变成模板类。
STL
vector内部是动态数组实现
for(vector<TYPE>::iterator it=v.begin();it!=v.end();i++)
for(vector<TYPE>::reverse_iterator it = v.rbegin();it!=v.rend();i++)
deque:内部估计是双向链表
queue:底层是deque
list是双向链表
priority_queue:最大堆,底层为vector
stack:底层为vector
multiset(有重复元素还叫集合么????)、set底层估计会排序二叉树,对于set重复的元素不会插入set中。
multimap: