C++的基础学习书籍有《C++ Primer》Lippman,提升学习《Effective C++》Scott Meyers,《深度探索C++对象模型》Lippman。通过这三本书可以基本上学习C++的核心概念和使用方法。
1.《C++ Primer》(第4版)
(1)第一章 快速入门
1. C++和C语言一样,操作系统仍然是通过调用main函数来执行程序,但是C++允许编写类文件,里面不存在main函数,但是类文件的方法调用的最终入口还是在main函数中。
2. 对于main函数,返回类型必须是int型,因此return必须返回一个int值。返回0通常表示main函数成功执行完毕,非0通常表示有错误出现。
3. C++编译器常用的有GNU(UNIX/LINUX)和Visual Studio,GNU常用g++ .... -o来编译,visual studio常用cl -GX命令来编译。
4. C++常用的I/O对象存在于istream,ostream库中,分别是cin,cout,clog,cerr。大部分操作系统提供了重定向输入输出流的方法,默认是执行窗口,通过重定向可以将这些流和其他文件(设备)联系。
5. #include头文件时,有两种表示方式,标准库的头文件用尖括号< >括起来,非标准库的头文件用双引号“”括起来。
6. 输出语句std::cout之后应该刷新输出流,通过endl可以完成输出换行的效果,刷新与设备相关的缓冲区。通过刷新缓冲区,用户可以立即看到写入到流中的输出。忘记刷新输出流可能会造成输出停留在缓冲区。
7. std是命名空间,使用命名空间是为了避免程序员由于无意中使用了与库中所定义名字相同的名字而引起冲突。::是作用域操作符,表示使用的是定义在命名空间some中的some。
8. std::cin>>v1>>v2;等价于std::cin>>v1; std::cin>>v2; >>操作符返回左操作数作为结果。
9. C++的注释有两种,一种是//,一种是注释对/**/。注意在使用注释对时一个注释对不能出现在另一个注释对中,否则会导致编译出错。临时忽略一段代码更好的方法是采用单行注释//。
10. C++中while(std::cin>>value)和C语言中相似while(scanf(“%d”,&value))相似,scanf(“%d”,&value)返回1表示当前有一个输入正确,返回0表示错误。当遇到文件结束符EOF或者无效输入时,会导致当前输入流无效。Windows下常用Ctrl+z表示EOF,unix中常用Ctrl-d。
11. 调用操作符是()
(2)第二章 变量和基本类型
1. C++内置的数据类型分为整型[整数int,布尔值bool],字符型[字符char和wchar_t:用于扩展字符集比如汉字和日语]和浮点型[float,double]。int为一个字长(4bytes 常常是16bit),short半个字长,long是1~2个字长。对于有符号和无符号型,默认是int,short,long是带符号的,带符号的类型表示的数绝对值范围比较小。对unsigned来说,负数超出其取值范围,但是C++允许把负数赋值给unsigned类型,处理的方式是该负数对该类型的取值个数求模后的值。使用unsigned类型比较明智,可以避免越界导致负数的可能性。
2. float(32bit 一个字长),double(64bit 两个字长)。float只能保证6位有效数字,double至少可以保证10位有效数字。使用double类型基本上不会出错。
3. 字面值整数常量默认类型是int或long,没有short类型的。unsigned和long可以通过加L和U后缀。默认的浮点字面值常量是double类型。F后缀表示单精度。当需要表示wchar_t时,在字符前面加上L,如L’a’。
4. 转义字符,所有的转义字符可以表示为通用的转义字符,如\ooo表示三个八进制数字,在这样的情况下\12表示换行符\n。\0表示空字符,\xaaa表示aaa十六进制。
5. C++中所有字符串常量(“ ”)都会自动在末尾加一个空字符。宽字符串常量同样是在前面加前缀L。当字符串常量和宽字符串常量连接时结果未定义,由编译器决定。
6. 在一行的末尾加一个反斜线符号可将此行和下一行当做同一行处理。
7. C++所有的标识符大小写敏感。标识符不能包含两个连续的下划线,不能以下划线开头后跟一个大写字母。
8. C++中初始化不是赋值,初始化时创建变量并赋初值,赋值是擦除对象的当前值并用新值替代。初始化有两种方式,复制初始化int ival(1024);直接初始化int ival = 1024;直接初始化更加灵活,效率更高。
9. 在函数体外定义的内置类型变量会自动初始化为0,内部定义的不自动初始化。
10. 变量的定义和声明的区别:定义需要为变量分配存储空间,有且仅有一个定义。但是可以有多个声明,声明用于向程序表明变量的类型和名字。定义包含了声明,可以单独通过extern关键字声明变量而不定义它。它不会分配储存空间,只是说明变量在程序的其他的地方。只有当声明也是定义时,才可以有初始化式。
int a; //定义并声明a
extern int a; //声明i
extern int a = 1; //定义a,当声明有初始化式时,可以被当做定义。只有当extern声明位于函数外部时,才可以含有初始化式。
任何在多个文件中使用的变量都需要有与定义分离的声明,这样一个文件含有变量的定义,使用该变量的其他文件中包含变量的声明。
提倡在变量使用处定义变量,这样可以提高程序的可读性。
11. const修饰变量表示定义了一个常量,常量必须在定义的时候初始化,并且定义之后不能被修改。const变量在全局作用域中声明之后,不能被其他文件以extern的形式访问。但是在定义const变量的前面加上extern则可以在其他文件中访问到这个const变量。
举例:file1中:extern const int a = 1;则在file2中通过声明 extern const int a;可以访问当file1中的a常量。
12. 引用必须被初始化,并且必须被初始化为与该引用同类型的对象。作用在引用上的所有操作实际都作用在该引用绑定的对象上。引用一旦初始化,不能将引用绑定到另一对象。也就是说引用是不能重新赋值的。一旦初始化时指定之后就不能更改了。但是引用可以通过赋值修改它的引用的值。
13. const引用:指向const对象的引用。由于这个引用指向的是const对象,所以只能读取不能修改这个引用指向的对象。将普通的引用绑定到const对象时不合法的。const int a = 1; const int &ival = a;
const引用可以初始化为不同类型的对象,或者初始化为右值。但是非const引用对象只能绑定到与该引用同类型的对象上。
const_iterator类型只能用于读取容器内的元素,但是不能改变容器内元素的值。也就是说不能对const_iterator迭代器解引用赋值,但是迭代器本身的值是可以改变的。
const的iterator和const_iterator不一样,需要区分开来,两者的使用方式和定义方式都不同。const的iterator表示一个const迭代器,迭代器本身的值是不可以改变的,但是它解引用后的值可以改变。同时,定义const的iterator和定义一个const变量一样,需要在定义的时候马上初始化。这点上的表现形式和const指针和指针的const正好相反。
Eg: vetor<int>::const_iterator ivec;
ivec = num.begin();
const vector<int>::iterator ivec2 = num2.begin();//必须初始化
ivec++; //ok
*ivec = 3; //error
ivec2++; //error
*ivec2 = 3; //ok
指向const对象的指针: const double *cptr;//cptr指向一个const double类型的对象,cptr本身是可以改变值的。相当于const_iterator。
const指针:int *const curErr = &errNumber;const指针是指针本身是const的,类似于const的iterator,必须初始化。指针本身不可以改变,但其指向的对象值可以改变。
typedef string *pstring;
const pstring cstr;
那么其实cstr指向的是string *const 类型,因为const用来修饰pstring,也就是说是一个const指针。
15. 枚举enum中用来初始化枚举成员的值必须是一个常量表达式。枚举成员只可以是不唯一的,即是可以是重复的枚举值。枚举类型对象的初始化或赋值只能通过其枚举成员或同一枚举类型的其他对象来进行。不能是枚举给的数值。
16. 一个容易被忽略的问题,定义的时候class xx {};后面必须要跟一个分号。
17. class与struct的区别,class默认是private,struct默认是public。
18. 头文件一般包含类的定义,extern 变量的声明和函数的声明。头文件用于声明而不是定义,但是它可以定义类,定义const对象和inline函数。
19. 使用预处理器定义头文件保护符,避免多次包含同一个头文件。预处理变量常用全大写字母表示,在程序中必须是唯一的。#ifndef .....#define....#endif
(3)第三章 标准库类型
1. 除了通过作用域操作符::来表示命名空间,一种比较简洁的方式是使用using声明。Eg: using namespace std; using std::cin;
2. 在头文件中最好总是使用完全限定的标准库的名字,这是因为头文件的内容会被复制到程序中,不管程序是否需要当前的using声明。
3. 标准库string: string s; cin>>s;会从非空白字符开始读取串储存到s中,并直到再次遇到空白字符为止。
string的几种初始方式:
string s1; //构造函数默认为空串
string s2(“hello”);
string s3(s2);
string s4(9,’a’);
getline可以每次读取一行,cin>>每次只能读取其中的一个单词。
string 常用的操作:
s.empty(); //返回bool表示是否为空
s.size(); //返回s中字符的个数,是string::size_type类型【unsigned】,不能把返回值赋值给int类型。
s[n]; //返回s中第n个位置的字符。n也是string::size_type类型
s1+s2
s1==s2
各种比较操作符用在string上进行string的对比操作。
在string对象和字符串常量相加的时候,+操作符的左右操作数必须至少有一个是string类型的。
char类型的字符操作库函数如isdigit,toupper都在cctype头文件中。
由于C++兼容C语言,一般当C语言中标准库头文件为name.h时,对应的C++头文件名是cname。同时cname定义的名字都定义在std中。
4. 标准库vector:vector是一个类模板,vector<type> name;中type定义当前vector模板中的内容。
vector的几种初始化方式:
vector<T> v1; //构造函数默认v1为空
vector<T> v2(v1);
vector<T> v3(n,i);
vector<T> v4(n);
vector 常用的操作:
v.empty();
v.size(); //返回vector<t>::size_type类型的大小
v.push_back(t);
v[n]; //n是vector<t>::size_type类型
v1==v2
v1=v2
其他比较操作符
vector的下标操作只能用于获取已经存在的元素,可以通过下标操作赋值, 也可以通过用push_back方法。但是需要区别的是下标操作不会添加任何元素。
5. 迭代器:下标和迭代器都是用于访问vector对象的方式。但是迭代器适用的范围更加广泛,对大多数标准容器都适用。
vector<int>::iterator iter; //定义了一个迭代器
容器的begin end操作赋值给迭代器,其中begin指向容器的第一个元素,end指向末端的下一个,即超出末端迭代器。当容易为空时,begin和end指向相等。
迭代器出了自增自减操作,还有iter+/-n操作,其中n四对应容器的size_type或difference_type类型和iter1-iter2操作,得到的值是difference_type(signed)类型的。
任何改变容器长度的操作如push_back都会使已有的迭代器值失效,因此需要重新赋值。
6. bitset类型,bitset也是一种类模板,但是它区别的是unsigned常量表示的长度而不是数据类型,它可以用来更加方便的处理位集。通常我们用<<和>>操作符来处理。
bitset初始化的几种方式:
bitset<n> b;
bitset<n> b(u); //u是unsigned long型
bitset<n> b(s); //s是string对象
bitset<n> b(s,pos,n); //s中从pos开始的n位
对初始化过程具体说明:
unsigned值赋值给bitset对象时,可以不是long类型,也可以是int类型的。但是在位数的取舍上和处理上是一致的。赋值时,该值将转化成二进制。当bitset length>unsigned时,前面高阶补0;当bitset length<unsigned时,取unsigned的低阶,丢掉高阶。
string对象初始化bitset对象时,从string对象读入位集的顺序是从右向左。就是说string对象和bitset对象之间是反转的。其实就是string中按照数组来看的低位在bitset的高位。其实还是相当于把string中的数据按照原来的位顺序赋值给了bitset。
比如说 string S("1100");
bitset<16> sSs(S);
cout<<sSs<<endl; //输出的结果是00000.....1100。可以知道在string中1是S[0],但它确出现在了bitset的高阶位置。
bitset上面的操作:
b.any(); //是否存在1的bit
b.none(); //是否不存在1的bit
b.cout(); //1的bit的位数,位数是size_t类型
b.size(); //b中二进制的个数,位数是size_t类型
b.set(); //所有bit置1
b.reset(); //所有bit置0
b.set(pos); //pos置1
b.reset(pos); //pos置0
b.flip(); //b中所有二进制位取反
b.flip(pos); //b中二进制位pos取反
b.test(pos); //b中二进制位pos是否是1
b [pos]; //取pos的bit值
b.to_ulong(); //将bit转化成unsigned long类型。仅当bitset的长度<=unsigned long时才使用to_ulong。否则会报overflow错误。
os<<b; //输出所有的二进制位
pos不是特殊类型,int就可以
(4)第四章 数组和指针
1. 内部数组和指针相对于容器和迭代器来说具有更快的速度,但是安全性较低。因此如果不强调速度,一般选择容器和迭代器更好。
2. 数组类型不能定义成引用,可以是任何内置类型、类类型或者复合类型。
3. 数组的维数比C语言要求更加严格,必须是const整型,枚举常量和整型字面值常量。//在dev C++上面测试,非const的变量只要赋值了就可以定义为size
4. 用””字符串赋值给字符数组时会多出一个默认的\0空字符,在定义长度的时候需要加上。但是如果是多个字符就不存在。
5. 数据下标的正确类型定义是size_t,和bitset一样。//dev c++编译器对此也没有报错,用int类型也是可以的
6. 指针如果不能马上初始化,也要将其值置为0或者NULL,这样可以避免使用未初始化指针带来的错误。
7. void类型的指针,可以保存任何类型对象的地址。但是它只支持几种操作,第一与其他指针比较,第二想传递void*函数传递void*指针或者返回void指针,第三给另一个void指针赋值。
8. 两个指针的差值类型是ptrdiff_t
9. 指针和引用的区别,引用必须初始化,引用改变的是其绑定的对象的值。指针不用初始化,它改变的是本来自己的值。
10. C++中保存了C语言中处理字符串的库函数,库函数的名字是<cstring>,是原来C语言中的string.h的头文件的C++版本。包括的函数有strlen,strcat,strcmp。这些函数获取的是char类型数组的指针,需要注意的是strlen的执行过程是遇到’\0’之后,才会中断程序返回长度,因此char类型数组指针指向的对象一定要有空结束字符。传递给strcat和strcpy的第一个实参要有足够大的空间存放新串。由于使用strcat,strcpy可能会有缓冲区溢出的安全隐患,所以非要用C风格字符串,最好用strncat,strncpy,这两个函数可以控制复制字符的个数。最后的结论就是最好采用string类型,比C风格字符串处理更加简单高效。
11. 区别程序存放的几个内存形式:
(1)堆:称为动态内存分配,C语言使用malloc和free在自由存储区中分配的存储空间,C++使用的是new和delete实现同样功能动态分配的对象会被存放在堆中。堆的创建和销毁需要程序员自己负责,堆的创建速度相比于栈来说要慢很多,因为堆在创建的时候需要搜索。堆的空间一般比较大。
(2)栈:栈中存放的时候局部变量,函数的变量等信息,就是我们一般常用的这些变量。栈的创建速度很快,但是栈空间一般很小,只有1~2MB.
(3)静态存储区:静态存储区一般存放所有的static对象,全局变量对象。这块内存在程序编译时就已经存在,在程序整个运行期间都存在。
(4)文字常量区:文字常量例如字符串常量区
下面给出一个例子:
#include <string>
int a=0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b;//栈
char s[]="abc"; //栈
char *p2; //栈
char *p3="123456"; //123456\0在常量区,p3在栈上。
static int c=0; //全局(静态)初始化区
p1 = (char*)malloc(10);
p2 = (char*)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456\0"优化成一个地方。
}
12. 动态创建数组:通常因为在编译的时候无法知道数组维数才定义动态数组。
Eg: int *pia = new int[10]; new表达式返回指向新分配数组的第一个元素的指针。此时数组对象没有名字,只能通过指针地址间接访问堆中的对象。如果分配时采用的数组元素是类类型,则调用默认构造函数初始化;如果是内置类型,则无初始化。当数组对象的类型是const时,则必须要对数组对象做初始化操作。这时可以采用const int *a = new const int[10]();完成初始化操作。
调用new动态创建长度为0的数组是合法的。
C++动态数组释放的方式是delete [] pa;这个空方括号对是必不可少的,否则释放的是单个对象,而不是数组。容易造成内存泄露。
13. C风格的字符串可以赋值给string类型,但是string类型不能直接给char *类型,必须要通过c_str()方法返回指向const char类型的数组。Eg:const char *str = st2.c_str();// st2是string类型
14. 可以使用数组初始化vector对象,但是第一个参数是用于初始化的第一个元素,第二个参数是最后一个元素下一个位置的地址。但是不允许数组初始化数组。
15. 多维数组的指针表示:
int *ip[4];//指针构成的数组,每个指针指向一个int对象
int (*ip)[4];//一个指针,指向的内容是int [4]的数组
int &arr[10];//10个引用构成的数组
int (&arr)[10];//10个int构成的数组的引用
16. 指针函数与函数指针:
int (*f)(x,y) :这是一个指向int XX(x,y)类型的函数指针,本质是一个指针,指向一个返回int型,由两个参数的函数。
int *f(x,y):这是一个指针函数,本质是一个函数,函数f返回的是int *。
typdef的用法
技术贴1:
用途一:
定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。比如:
char* pa, pb; // 这多数不符合我们的意图,它只声明了一个指向字符变量的指针,
// 和一个字符变量;
以下则可行:
typedef char* PCHAR;
PCHAR pa, pb;
这种用法很有用,特别是char* pa, pb的定义,初学者往往认为是定义了两个字符型指针,其实不是,而用typedef char* PCHAR就不会出现这样的问题,减少了错误的发生。
用途二:
用在旧的C代码中,帮助struct。以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名对象名,如:
struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;
而在C++中,则可以直接写:结构名对象名,即:tagPOINT1 p1;
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时
候,或许,在C++中,typedef的这种用途二不是很大,但是理解了它,对掌握以前的旧代
码还是有帮助的,毕竟我们在项目中有可能会遇到较早些年代遗留下来的代码。
用途三:
用typedef来定义与平台无关的类型。
比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
typedef long double REAL;
在不支持 long double 的平台二上,改为:
typedef double REAL;
在连 double 都不支持的平台三上,改为:
typedef float REAL;
也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健。
这个优点在我们写代码的过程中可以减少不少代码量哦!
用途四:
为复杂的声明定义一个新的简单的别名。方法是:在原来的声明里逐步用别名替换一部
分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化
版。举例:
原声明:void (*b[10]) (void (*)());
变量名为b,先替换右边部分括号里的,pFunParam为别名一:
typedef void (*pFunParam)();
再替换左边的变量b,pFunx为别名二:
typedef void (*pFunx)(pFunParam);
原声明的最简化版:
pFunx b[10];
原声明:doube(*)() (*e)[9];
变量名为e,先替换左边部分,pFuny为别名一:
typedef double(*pFuny)();
再替换右边的变量e,pFunParamy为别名二
typedef pFuny (*pFunParamy)[9];
原声明的最简化版:
pFunParamy e;
理解复杂声明可用的“右左法则”:从变量名看起,先往右,再往左,碰到一个圆括号
就调转阅读的方向;括号内分析完就跳出括号,还是按先右后左的顺序,如此循环,直
到整个声明分析完。举例:
int (*func)(int *p);
首先找到变量名func,外面有一对圆括号,而且左边是一个*号,这说明func是一个指针
;然后跳出这个圆括号,先看右边,又遇到圆括号,这说明(*func)是一个函数,所以
func是一个指向这类函数的指针,即函数指针,这类函数具有int*类型的形参,返回值
类型是int。
int (*func[5])(int *);
func右边是一个[]运算符,说明func是具有5个元素的数组;func的左边有一个*,说明
func的元素是指针(注意这里的*不是修饰func,而是修饰func[5]的,原因是[]运算符
优先级比*高,func先跟[]结合)。跳出这个括号,看右边,又遇到圆括号,说明func数
组的元素是函数类型的指针,它指向的函数具有int*类型的形参,返回值类型为int。
这种用法是比较复杂的,出现的频率也不少,往往在看到这样的用法却不能理解,相信以上的解释能有所帮助。
*****以上为参考部分,以下为本人领悟部分*****
使用示例:
1.比较一:
#include <iostream>
using namespace std;
typedef int (*A) (char, char);
int ss(char a, char b)
{
cout<<"功能1"<<endl;
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
int bb(char a, char b)
{
cout<<"功能2"<<endl;
cout<<b<<endl;
cout<<a<<endl;
return 0;
}
void main()
{
A a;
a = ss;
a('a','b');
a = bb;
a('a', 'b');
}
2.比较二:
typedef int (A) (char, char);
void main()
{
A *a;
a = ss;
a('a','b');
a = bb;
a('a','b');
}
两个程序的结果都一样:
功能1
a
b
功能2
b
a
*****以下是参考部分*****
参考自:http://blog.hc360.com/portal/personShowArticle.do?articleId=57527
typedef 与 #define的区别:
案例一:
通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:
typedef char *pStr1;
#define pStr2 char *;
pStr1 s1, s2;
pStr2 s3, s4;
在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们
所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一
个类型起新名字。
案例二:
下面的代码中编译器会报一个错误,你知道是哪个语句错了吗?
typedef char * pStr;
char string[4] = "abc";
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++;
是p2++出错了。这个问题再一次提醒我们:typedef和#define不同,它不是简单的
文本替换。上述代码中const pStr p2并不等于const char * p2。const pStr p2和
const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类
型是我们自己定义的而不是系统固有类型而已。因此,const pStr p2的含义是:限定数
据类型为char *的变量p2为只读,因此p2++错误。
typedef的作用:1)用typedef简化指向多维数组的指针的定义(C++primer P124)2)用typedef简化函数指针的定义(C++primer P237)3) 用typedef简化复杂类型的定义,如pair类型和map的value_type(C++primer P307,P313)
下面转自http://book.douban.com/annotation/13487433/
技术贴2:对typedef用法的领悟:
以前一直以为typdef的用法不过是typedef A B就是把类型B作为类型A的一个新名字。但是碰到像typedef string *pstring或者是typedef int int_array[4]这样的定义就比较傻眼。
然后慢慢摸爬滚打明白了typedef的精髓。那就是typdef,定义什么就是什么。
比如定义一个string类型的指针变量,是string *str1;这个时候str1是变量名。
如果把这句话前面加上一个typdef,也就是typedef string *str2;这个时候str2就不是变量名,而是类型名,它的类型就是变量str1所具有的类型。也就是string *类型。
所以typedef string *pstring这句话的意思就豁然开朗了,那么我以后可以拿pstring去定义别的变量,不如pstring pstr;这就定义了一个指向string的指针对象pstr。
再比如typedef int int_array[4];如果去掉前面的typedef那么定义的是一个叫做int_array的含有4个元素的数组。前面加上typedef以后,int_array就变成了含有4个元素的数组类型的替代名。以后要有int_array ia;这句话我们就知道它等同于int ia[4];
再比如这一页的指向函数的指针:
bool (*pf)(const string&, const string&)
使用typdef以后,定义这一类型(即同样的形参以及同样的返回类型)的函数指针语句都将得到简化。方法如下:
typedef bool (*cmpFcn)(const string&, const string&);
用新得到的类型来定义两个新的函数指针pf1和pf2,它们所指向的函数都有两个const string&形参且返回bool类型值。以下是定义:
cmpFcn pf1;
cmpFcn pf2;
(5)第五章 表达式
1.求余操作的操作数只能是整型,也就是说bool ,int,char的类型。如果两个操作数为正,则/和%结果也是正。两个为负,则除法的结果是正,求余的结果是负数。如果是一正一反,那么求余结果的正负需要看编译器,除法的结果是负数。
2.短路求值:(|| &&)逻辑与和逻辑或操作符总是先计算左操作符,然后计算右操作符.只有当左操作数的值无法确定逻辑表达式的结果时才求解右操作符。
3.关系操作符(<,<=,>,>=)具有左结合特性,不应该串接使用。Eg:if(i<j<k),相当于if((i<j)<k),那么等效于k>0|1
4.bool可转换成任何算术类型,
当val是bool时,可写成if(val)
当val不是bool时,可写成if(val == 1),但是不能写成if(val),因为这表示val是任何非0就成功。
5.位操作符<<和>>的数据类型可以是有符号的,也可以是无符号的。如果是有符号的负数,则操作符对符号位的处理取决于机器。由于系统不能确保对符号位的处理,因此在使用位操作符时,操作数最好采用unsigned类型的。
6.重载操作符与内置版本拥有相同的优先级和结合性。
移位操作符就和IO操作符是重载关系,移位操作符具有中等优先级,其优先级比算术操作符低,但是比关系操作符、赋值操作符和条件操作符高。具有左结合特性。
Eg:cout<<42+10; // 最后输出52
cout<<10<42; //执行过程是cout<<10的结果与42比较
7.左结合性和右结合性解释:右结合性是指当有多个相同操作符连接多个操作数时,会从右向左结合。左结合性则刚好相反。
8.自增、自减操作:前自增,后自增。常考点。
9.解引用*的优先级低于后自增++,但是高于.点操作符。
10.sizeof返回一个对象或类型名的长度,返回值的类型是size_t.
sizeof返回的结果依赖于当前的类型:
(1) char或者值为char类型的返回1
(2)引用类型返回存放引用类型对象所需的内存大小,即当前引用的类型对象所需的内存大小。例如如果引用的是char类型的,那么就是1;引用的是string类型的,就是8;如果是int,就是4;
(3)指针返回的是存放指针所需的内存大小,要获取指针指向的对象的大小要解引用。
(4)对数组会返回sizeof(操作的结果)*数组元素个数
11.逗号表达式从左向右计算,是最右边表达式的值。
12. C++操作符的优先级和结核性;
13.new操作符返回的是指针
14.当new表达式无法获取需要的内存时,系统将抛出bad_alloc的异常。
15.只有用new操作符分配的指针才能用delete操作符释放内存。
16.delete操作是释放指针指向的内存空间的功能,在执行完之后,该指针仍然指向被删除的内存空间的位置,所以要把指针置为0,表明不再指向任何对象。
17.数据类型转换:
(1)隐式类型转换:[不需要程序员了解和指定的转换方式,由编译器自动完成]
l 在赋值操作时,左右操作数类型不同,右操作数会被转化成左边的类型。
l 包含signed和unsigned int型的表达式,会将signed转化成成unsigned类型
l 将操作数转化成表达式中的最大类型(char->short->int->long)
当char+short类型时转化成int
short等价于 short int ; long等价于long int;
l 枚举类型对象或枚举成员提升类型取决于机器定义,至少提升为int,可能是unsigned int,long等。
l 当用非const对象初始化const对象时,系统默认将非const对象转换成const类型
(2)显式类型转换:C++有4中强制转换的方式:
整个方式是cast_name<type>(expression);其中cast_name是强制转换的类型,type是转换后的类型,expression中存放的是要进行类型转换的值。
l static_cast: 隐式转化的都可以由static指定,也可以将较大的类型转换成较小的类型。在编译的时候就回检查类型是否有关系,至少是指针到指针,实例到实例,不能是指针到实例这样的转换。
l dynamic_cast:在动态运行的时候动态检查当前的类型。其type只能是指针或引用。它有两个重要的约束条件,其一是要求new_type为指针或引用,其二是下行转换时要求基类是多态的(基类中包含至少一个虚函数)。
l const_cast:添加和删除const特性的强制转换
l reinterpret_cast:可以执行任何类型的类型转换,但是在底层不进行转换,只是进行了重新解释。这是最危险的类型转换。其type只能是指针或引用。
18.旧式强制类型转换:
有两种风格:type(expr)和(type)expr
技术贴1:
reinterpret_cast可以转换任意一个32bit整数,包括所有的指针和整数。可以把任何整数转成指针,也可以把任何指针转成整数,以及把指针转化为任意类型的指针,威力最为强大!但不能将非32bit的实例转成指针。总之,只要是32bit的东东,怎么转都行!
static_cast和dynamic_cast可以执行指针到指针的转换,或实例本身到实例本身的转换,但不能在实例和指针之间转换。static_cast只能提供编译时的类型安全,而dynamic_cast可以提供运行时类型安全。举个例子:
class a;class b:a;class c。
上面三个类a是基类,b继承a,c和ab没有关系。
有一个函数void function(a&a);
现在有一个对象是b的实例b,一个c的实例c。
function(static_cast<a&>(b)可以通过而function(static_cast<a&>(c))不能通过编译,因为在编译的时候编译器已经知道c和a的类型不符,因此static_cast可以保证安全。
下面我们骗一下编译器,先把c转成类型a
b& ref_b = reinterpret_cast<b&>c;
然后function(static_cast<a&>(ref_b))就通过了!因为从编译器的角度来看,在编译时并不能知道ref_b实际上是c!
而function(dynamic_cast<a&>(ref_b))编译时也能过,但在运行时就失败了,因为dynamic_cast在运行时检查了ref_b的实际类型,这样怎么也骗不过去了。
在应用多态编程时,当我们无法确定传过来的对象的实际类型时使用dynamic_cast,如果能保证对象的实际类型,用static_cast就可以了。至于reinterpret_cast,我很喜欢,很象c语言那样的暴力转换:)
dynamic_cast:动态类型转换
static_cast:静态类型转换
reinterpret_cast:重新解释类型转换
const_cast:常量类型转换
专业的上面很多了,我说说我自己的理解吧:
dynamic_cast一般用在父类和子类指针或应用的互相转化;
static_cast一般是普通数据类型(如int m=static_cast<int>(3.14));
reinterpret_cast很像c的一般类型转换操作
const_cast是把cosnt或volatile属性去掉
技术贴2:
static_cast, dynamic_cast, reinterpret_cast, const_cast区别比较
隐式转换(implicit conversion)
short a=2000;
int b;
b=a;
short是两字节,int是四字节,由short型转成int型是宽化转换(bit位数增多),编译器没有warning,如下图所示。宽化转换(如char到int,int到long long,int到float,float到double,int到double等)构成隐式转换,编译器允许直接转换。
但若反过来
double a=2000;
short b;
b=a;
此时,是从8字节的double型转成2字节的short型变量,是窄化转换,编译器就会有warning了,如下所示,提醒程序员可能丢失数据。不过需要注意的是,有些隐式转换,编译器可能并不给出warning,比如int到short,但数据溢出却依然会发生。
C风格显式转换(C style explicit conversion)
要去掉上述waring很简单,熟悉C语言的程序员知道,有两种简单的写法(C风格转换与函数风格转换):
double a=2000.3;
short b;
b = (short) a; // c-like cast notation
b = short (a); // functional notation
如下图所示,此时warning就没了
这种显式转换方式简单直观,但并不安全,举一个父类和子类的例子如下:
// class type-casting
#include <iostream>
using namespace std;
class CDummy {
float i,j;
CDummy():i(100),j(10){}
};
class CAddition:public CDummy
{
int *x,y;
public:
CAddition (int a, int b) { x=&a; y=b; }
int result() { return *x+y;}
};
int main () {
CDummy d;
CAddition * padd;
padd = (CAddition*) &d;
cout << padd->result();
return 0;
}
编译器不报任何错,但运行结果出错,如下图所示:
究其原因,注意这一句:padd = (CAddition*) &d;
此时父类的指针&d被C风格转换方式强制转成了子类的指针了,后面调用了子类的方法result,需要访问*x,但指针指向的对象本质还是父类的,所以x相当于父类中的i,y相当于父类中的j,*x相当于*i,但i是float型变量(初始化为100),不是地址,所以出错,如果程序员正是鲁莽地对这个地址指向的内存进行写入操作,那将可能会破坏系统程序,导致操作系统崩溃!
这里有一个重要概念,CAddition*是子类的指针,它的变量padd可以调用子类的方法,但是它指向的是父类的对象,也就是说padd指向的内存空间里存放的是父类的成员变量。深入地说,数据在内存中是没有“类型”一说的,比如0x3F可能是字符型,也可能是整型的一部分,还可能是地址的一部分。我们定义的变量类型,其实就是定义了数据应该“被看成什么”的方式。
因此padd类指针实质是定义了取值的方式,如padd->x就是一并取出内存空间里的0号单元至3号单元的值(共4个字节),将其拼成32位并当作指针,padd->y则取出内存空间里的4号单元至7号单元(共4个字节),将其拼成32位并当作int型变量。但实际上padd指向的是父类的对象,也就是前4个字节是float型变量,后4个字节也是float型变量。
从这里可以看出,程序员的这种转换使编译器“理解”出错,把牛当成马了。
从上可见,用C风格的转换其实是不安全的,编译器无法看到转换的不安全。
上行转换(up-casting)与下行转换(down-casting)
看到这个,读者可能会问,哪些转换不安全?根据前面所举的例子,可以看到,不安全来源于两个方面:其一是类型的窄化转化,会导致数据位数的丢失;其二是在类继承链中,将父类对象的地址(指针)强制转化成子类的地址(指针),这就是所谓的下行转换。“下”表示沿着继承链向下走(向子类的方向走)。
类似地,上行转换的“上”表示沿继承链向上走(向父类的方向走)。
我们给出结论,上行转换一般是安全的,下行转换很可能是不安全的。
为什么呢?因为子类中包含父类,所以上行转换(只能调用父类的方法,引用父类的成员变量)一般是安全的。但父类中却没有子类的任何信息,而下行转换会调用到子类的方法、引用子类的成员变量,这些父类都没有,所以很容易“指鹿为马”或者干脆指向不存在的内存空间。
值得一说的是,不安全的转换不一定会导致程序出错,比如一些窄化转换在很多场合都会被频繁地使用,前提是程序员足够小心以防止数据溢出;下行转换关键看其“本质”是什么,比如一个父类指针指向子类,再将这个父类指针转成子类指针,这种下行转换就不会有问题。
针对类指针的问题,C++特别设计了更加细致的转换方法,分别有:
static_cast <new_type> (expression)
dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
const_cast <new_type> (expression)
可以提升转换的安全性。
static_cast <new_type> (expression) 静态转换
静态转换是最接近于C风格转换,很多时候都需要程序员自身去判断转换是否安全。比如:
double d=3.14159265;
int i = static_cast<int>(d);
但static_cast已经有安全性的考虑了,比如对于不相关类指针之间的转换。参见下面的例子:
// class type-casting
#include <iostream>
using namespace std;
class CDummy {
float i,j;
};
class CAddition {
int x,y;
public:
CAddition (int a, int b) { x=a; y=b; }
int result() { return x+y;}
};
int main () {
CDummy d;
CAddition * padd;
padd = (CAddition*) &d;
cout << padd->result();
return 0;
}
这个例子与之前举的例子很像,只是CAddition与CDummy类没有任何关系了,但main()中C风格的转换仍是允许的padd = (CAddition*) &d,这样的转换没有安全性可言。
如果在main()中使用static_cast,像这样:
int main () {
CDummy d;
CAddition * padd;
padd = static_cast<CAddition*> (&d);
cout << padd->result();
return 0;
}
编译器就能看到这种不相关类指针转换的不安全,报出如下图所示的错误:
注意这时不是以warning形式给出的,而直接是不可通过编译的error。从提示信息里可以看到,编译器说如果需要这种强制转换,要使用reinterpret_cast(稍候会说)或者C风格的两种转换。
总结一下:static_cast最接近于C风格转换了,但在无关类的类指针之间转换上,有安全性的提升。
dynamic_cast <new_type> (expression) 动态转换
动态转换确保类指针的转换是合适完整的,它有两个重要的约束条件,其一是要求new_type为指针或引用,其二是下行转换时要求基类是多态的(基类中包含至少一个虚函数)。
看一下下面的例子:
#include <iostream>
using namespace std;
class CBase { };
class CDerived: public CBase { };
int main()
{
CBase b; CBase* pb;
CDerived d; CDerived* pd;
pb = dynamic_cast<CBase*>(&d); // ok: derived-to-base
pd = dynamic_cast<CDerived*>(&b); // wrong: base-to-derived
}
在最后一行代码有问题,编译器给的错误提示如下图所示:
把类的定义改成:
class CBase { virtual void dummy() {} };
class CDerived: public CBase {};
再编译,结果如下图所示:
编译都可以顺利通过了。这里我们在main函数的最后添加两句话:
cout << pb << endl;
cout << pd << endl;
输出pb和pd的指针值,结果如下:
我们看到一个奇怪的现象,将父类经过dynamic_cast转成子类的指针竟然是空指针!这正是dynamic_cast提升安全性的功能,dynamic_cast可以识别出不安全的下行转换,但并不抛出异常,而是将转换的结果设置成null(空指针)。
再举一个例子:
#include <iostream>
#include <exception>
using namespace std;
class CBase { virtual void dummy() {} };
class CDerived: public CBase { int a; };
int main () {
try {
CBase * pba = new CDerived;
CBase * pbb = new CBase;
CDerived * pd;
pd = dynamic_cast<CDerived*>(pba);
if (pd==0) cout << "Null pointer on first type-cast" << endl;
pd = dynamic_cast<CDerived*>(pbb);
if (pd==0) cout << "Null pointer on second type-cast" << endl;
} catch (exception& e) {cout << "Exception: " << e.what();}
return 0;
}
输出结果是:Null pointer on second type-cast
两个dynamic_cast都是下行转换,第一个转换是安全的,因为指向对象的本质是子类,转换的结果使子类指针指向子类,天经地义;第二个转换是不安全的,因为指向对象的本质是父类,“指鹿为马”或指向不存在的空间很可能发生!
最后补充一个特殊情况,当待转换指针是void*或者转换目标指针是void*时,dynamic_cast总是认为是安全的,举个例子:
#include <iostream>
using namespace std;
class A {virtual void f(){}};
class B {virtual void f(){}};
int main() {
A* pa = new A;
B* pb = new B;
void* pv = dynamic_cast<void*>(pa);
cout << pv << endl;
// pv now points to an object of type A
pv = dynamic_cast<void*>(pb);
cout << pv << endl;
// pv now points to an object of type B
}
运行结果如下:
可见dynamic_cast认为空指针的转换安全的,但这里类A和类B必须是多态的,包含虚函数,若不是,则会编译报错。
reinterpret_cast <new_type> (expression) 重解释转换
这个转换是最“不安全”的,两个没有任何关系的类指针之间转换都可以用这个转换实现,举个例子:
class A {};
class B {};
A * a = new A;
B * b = reinterpret_cast<B*>(a);//correct!
更厉害的是,reinterpret_cast可以把整型数转换成地址(指针),这种转换在系统底层的操作,有极强的平台依赖性,移植性不好。
它同样要求new_type是指针或引用,下面的例子是通不过编译的:
double a=2000.3;
short b;
b = reinterpret_cast<short> (a); //compile error!
const_cast <new_type> (expression) 常量向非常量转换
这个转换好理解,可以将常量转成非常量。
// const_cast
#include <iostream>
using namespace std;
void print (char * str)
{
cout << str << endl;
}
int main () {
const char * c = "sample text";
char *cc = const_cast<char *> (c) ;
Print(cc);
return 0;
}
从char *cc = const_cast<char *>(c)可以看出了这个转换的作用了,但切记,这个转换并不转换原常量本身,即c还是常量,只是它返回的结果cc是非常量了。
总结
C风格转换是“万能的转换”,但需要程序员把握转换的安全性,编译器无能为力;static_cast最接近于C风格转换,但在无关类指针转换时,编译器会报错,提升了安全性;dynamic_cast要求转换类型必须是指针或引用,且在下行转换时要求基类是多态的,如果发现下行转换不安全,dynamic_cast返回一个null指针,dynamic_cast总是认为void*之间的转换是安全的;reinterpret_cast可以对无关类指针进行转换,甚至可以直接将整型值转成指针,这种转换是底层的,有较强的平台依赖性,可移植性差;const_cast可以将常量转成非常量,但不会破坏原常量的const属性,只是返回一个去掉const的变量
(6) 第六章 语句
1.无关的空语句并非总是无害的
2.在switch中,如果default后面没有默认的操作,也要加上 ;空语句。case标号的后面必须是一个常量表达式
3.do while要以分号结束
4.当我们采用的是标准库中的库文件时采用<>,但是如果采用的是C版本的库文件要加上.h。例如#include<string>和#include<string.h>是有区别的,第一个是string类类型的库文件,而string.h是C语言中关于字符串处理的strlen等函数的实现库,它相当于#include<cstring>。
技术贴:
#include<string>与#include<string.h>的区别
为什么下面这段代码
#include <string.h>
void main()
{
string aaa= "abcsd d";
printf("looking for abc from abcdecd %s\n",
(strcmp(aaa,"abc")) ? "Found" : "Not Found");
}
不能正确执行,说是string类型没有定义
而下面:
#include <string>
using namespace std;
void main()
{
string aaa= "abcsd d";
printf("looking for abc from abcdecd %s\n",
(strcmp(aaa,"abc")) ? "Found" : "Not Found");
}
这里的string编译器就认识了,但是strcmp就不认识了呢?
---------------------------------------------------------------
一般一个C++的老的带“.h”扩展名的库文件,比如iostream.h,在新标准后的标准库中都有一个不带“.h”扩展名的相对应,区别除了后者的好多改进之外,还有一点就是后者的东东都塞进了“std”名字空间中。
但唯独string特别。
问题在于C++要兼容C的标准库,而C的标准库里碰巧也已经有一个名字叫做“string.h”的头文件,包含一些常用的C字符串处理函数,比如楼主提到的strcmp。
这个头文件跟C++的string类半点关系也没有,所以<string>并非<string.h>的“升级版本”,他们是毫无关系的两个头文件。
要达到楼主的目的,比如同时:
#include <string.h>
#include <string>
using namespace std;
或者
#include <cstring>
#include <string>
其中<cstring>是与C标准库的<string.h>相对应,但裹有std名字空间的版本。
5.异常处理:
try
{
//程序中抛出异常
throw value; //value可以是异常的对象,也可以是在try部分调用的某个函数中包含的throw操作
}
catch(valuetype v) //捕获throw到的数据类型
{
//例外处理程序段
}
在C++中没有finally关键字,但是在java中就有这个关键字,表示无论是否执行catch中的语句都要执行finally里面的操作。
throw到的异常类型可以是在以下四个头文件中定义的类型:
1.exception
2.stdexcept (定义了很多标准异常类,但是都只有what函数给出throw中相应类型时给的字符串)
3.new
4.type_info
try
{
throw runtime_error("djk");
}catch(runtime_error t){
t.what();//如果出错会打印出djk
}
<stdexcept>定义了一些标准的异常类。分为两大类:逻辑错误和运行时错误。其中运行时错误是程序员不能控制的。
逻辑错误都继承自logic_error:在运行前检测到的错误
异常 | 描述 |
domain_error | 域错误 |
invalid_argument | 非法参数 |
length_error | 通常是创建对象是给出的尺寸太大 |
out_of_range | 访问超界 |
运行时错误都继承自runtime_error:在运行的时候才能检测到的问题
异常 | 描述 |
overflow_error | 上溢 |
range_error | 超出表示范围 |
underflow_error | 下溢 |
6.预处理与断言:
当我们调试程序的时候,需要做一些调试操作,但是希望程序在正式运行的时候不再做这些操作,那么这个时候可以采用预处理。
#ifndef NDEBUG
//调试操作,NDEBUG可以是任意关键字
#endif
当正式运行的时候加上#define NDEBUG或者gcc -DNDEBUG main.cpp定义NDEBUG。
预处理器的四种调试用的常量:
__FILE__
__LINE__
__TIME__
__DATE__
断言的用途也是一样,它的头文件<assert.h>或者<cassert>中,当系统定义了NDEBUG之后assert的就不起作用了。否则当assert中的内容求解为false的时候就会终止程序。通常assert是用来测试不可能发生的情况。
(7) 第七章 函数
1.参数传递:函数的参数传递过程是这样的,创建形参变量,然后用实参值初始化形参变量。实参是传递的实参副本还是直接传递的实参值取决于当前的形参定义的是引用类型的还是非引用类型的。如果形参是非引用类型,那么传递的是实参的副本,在函数局部对形参的改变不会改变实参的值,即使是指针也是一样的。指针本身不会被改变,但是指针指向的内容可以被改变。如果形参是引用类型,那么传递的是实参的别名,在函数局部对形参的改变会改变实参的值。
具体的细节如下:
(1)非引用形参
当函数的形参是非const指针时,实参指针的值不改变,但是可以改变指针所指向对象的值。如果也不希望改变指针所指向对象的值,那么就定义为const指针。
例1:
void reset(const int *p)
{
std::cout<<*p<<std::endl; //ok,打印出1
*p = 2; //编译错误,assignment of read-only location '* p',此时也不能改变指针指向的内容的值
}
int main()
{
int b = 1;
int *p = &b;
reset(p); //可以将非const赋值给const的形参,一般不允许将const实参赋值给非const的形参。
}
例2:
void reset(int &p)
{
std::cout<<p<<std::endl;
}
int main()
{
const int a = 9;
reset(a); //不能将const int&转化成int &
}
例3:
void reset(int p)
{
std::cout<<p<<std::endl;
}
int main()
{
const int a = 9;
reset(a); //执行成功,打印出a的值
}
例4:
void reset(const int p)
{
std::cout<<p<<std::endl;
}
int main()
{
int a = 9;
reset(a); //执行成功,打印出a的值
}
例5:
void reset(const int &p)
{
std::cout<<p<<std::endl;
}
int main()
{
int a = 9;
reset(a); //执行成功,打印出a的值
}
从例2到例5可以看出,当形参是非引用的时候,const和非const的形参和实参之间可以相互赋值。但是如果是引用的形参,那么const类型的实参不能赋值给非const的引用类型。但是非const类型的实参可以赋值给const类型的引用。
(2)引用形参
C++中使用引用形参比指针更加安全和自然。
使用引用形参,函数可以无需复制直接访问实参对象.
-1 非const引用形参
-2 const引用形参: 如果使用引用形参的唯一目的是避免复制实参,也就是说只对实参就行读但是不进行修改,那么最后采用const引用形参。普通的非const引用形参在使用的时候不太灵活,这样的形参不能用const对象、字面值常量和产生右值的表达式进行实参初始化。
2.容器形参要么采用引用的方式防止复制,要么传递指向容器中需要处理的元素的迭代器来传递容器。
3.int main(int argc,char *argv[]),其实main函数是可以传递字符串数组进去的,char *argv[]就是char **agrc,指向由字符串数组构成的数组,如
argv[0] = “prol” ;argv[1]=”-d”; argc就是数组中字符串的个数。
4.不带返回值的return只能用于返回类型是void的函数,使用return;的作用是为了引起函数的强制结束,用法类似于break。
5.在含有return语句的循环后没有提供return语句很危险,很多编译器不能检查出这个错误,可能会造成运行时的不定错误。
6.函数的返回值当返回的是非引用类型的时候需要进行复制副本,但是如果返回的是引用类型时不需要复制返回值。但是需要注意的是无论是返回的引用类型还是指针类型,千万不能返回局部变量的引用,因为函数执行完毕之后,将释放分配给局部对象的存储空间,那么引用会指向不确定的内存。
7.主函数main不能调用自身,也不能被重载
8.函数和变量一样都需要先声明,再使用。需要注意的是函数声明的形参列表中,形参的名字是可以忽略的,只给出类型就可以,即int add (int = 2, int =3); 这样的声明形式是可以的。另外可以定义默认实参,如果在形参列表中有一个定义了默认实参,那么所有的参数都必须定义默认实参,即int add (int = 2, int );的表示时错误的。 通常会在函数声明中指定默认实参,并将声明放在合适的头文件中。
9.静态局部对象static 类型 变量名,可以跨越函数调用的生命期,这个对象一旦创建,那么在函数结束时静态局部变量会继续保持它的值,在程序结束前不会被撤销。但是需要注意的是static局部对象需要确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。静态局部变量如果没有初始化编译器会自动将其初始化为0.
10.内联函数inline,在定义函数时的前面加上inline表明当前这个函数时内联函数,它的作用的减少调用函数时带来的切换开销,直接在程序调用位置展开被调用函数的执行代码。但是需要注意的是,内联函数一般都用于那些比较小,自有几行代码但是常被调用的函数,对那种递归函数一般不选择将其定义为inline。这样做会展开很多行代码,达不到优化的效果。另外,内联函数一般在头文件中直接定义。
11.编译器会隐式的将在类中定义的成员函数当作是内联函数。
12.在定义成员函数的时候有时候会在形参列表后面加上const关键字,这是表示在我们使用默认的形参this指向当前类对象的时候,this指针是一个指向cosnt对象的指针,即当前成员函数的操作不会改变当前类中内容。这个函数被称为常量成员函数。
Eg: double avg_price() const;
常量成员函数使用const关键字说明的函数。
常量成员函数不更新对象的数据成员。
只有非静态成员函数才能是常量成员函数(对象属性)。
const不能用于构造、析构(程序执行不警告)。
13.函数不能仅仅因为返回值不能而实现重载,而是要指定不同的参数。
14.指向函数的指针:就是指向函数而非对象的指针。常用的表示如:
bool (*p)(string &,string &); //表示一个函数指针指向一个函数,这个函数的参数是两个string类型的引用,返回值是bool类型。通常为了表示方便常常用typedef bool (*p)(string &,string &); 来定义指针。
可以直接用函数名来给函数指针赋值,例如定义这样一个函数bool cmpares (string &,string &);那么p = cmpares,表示p指向cmpares这个函数。直接引用函数名此时等效于在函数名上应用取地址操作符。
15.函数指针只能通过同类型的函数或函数指针或0值常量表达式进行初始化或者赋值。
16.通过函数指针可以调用他所指向的函数,可以不需要直接解引用操作符,直接通过指针调用函数。例如前面定义的函数指针p,可以直接p(“hi”,”bye”);调用函数。
17.函数指针也可以做形参,有两种定义形参的方式。如
void use(bool(string &,string &));
void use(bool(*)(string &,string &));
18.函数指针也可以作为返回值类型,但是不好识别,例如
int (*ff(int))(int *,int);//需要从里向外看,这个函数时ff,形参是int,返回值是int*,int构成形参,int为返回值的函数指针。其实int (*ff(int))(int *,int);等价于typedef int (*pf)(int *,int); pf ff(int);
19.当有形参或者返回值是函数指针时,形参可以是直接用函数名作为实参传递给形参,但是返回类型不可以,必须要指明是指针。例如:
typedef int func(int*,int); //typedef没有的话会报错,为什么?
void f1(func);
func *f2(int);
(8) 第八章 标准I/O库
I/O标准库的头文件有
#include <iostream>
#include <ostream>
#include <istream>
#include <fstream>
#include <sstream>
1.这些I/O库都定义在std命名空间里面,因此在使用的时候必须要加上std命名空间。fstream是用于文件流,iostream是标准输入输出的库,sstream是读写内存中string流的库。其实iostream是stringstream和fstream的父类,但是stringstream的头文件是sstream,istream是istringstream和ifstream的父类,ostream是ostringstream和ofstream的父类。不过我们在实际定义的时候使用到这些类名,头文件却不同。类istringstream和ostringstream的头文件是sstream,ifstream和ofstream的头文件是fstream。
2.前面提到的流都是char类型的流,但是C++中还可能存在wchar_t类型,它的头文件分别是在char类型流的头文件前面加”w”,例如wiostream,但是在DEV C++中没有这个头文件存在。但是在定义了iostream头文件之后可以直接使用wiostream类。其对应的标准对象分别是wcin,wcout,wcerr。
3.我们知道的iostream,fstream,sstream都是流的类,在C++中针对标准输入输出定义了一些iostream类的对象cin,cout和cerr。<<和>>是输入输出的操作符。
4.I/O对象例如cin,cout,cerr或者我们自己定义的对象,和引用一样不允许赋值或复制操作。所以都不能是容器的内部类型,另外函数的形参或者返回类型也不能是流类型,只能是流对象的引用或者指针,而且这个引用和指针必须是非const的,因为读写会修改其状态。
5.对流状态的检测和管理:流的各个类中定义了iostate表示流的状态,当流的某些部分失败时,iostate的某些位置为1.badbit是被破坏的流的iostate的值,failbit是失败I/O的流的iostate的值,eofbit是到达文件结束符的iostate的值。这些类分别定义了相应的方法用于检测当前流的状态是否正常,这些方法有eof(),fail(),bad(),good(),clear(),clear(flag),setstate(flag),rdstate(),功能分别如下:
eof():是否到文件尾
fail():是否失败
bad():是否被破坏
good():是否是好的
clear():重设流状态
clear(flag):将流状态重设成flag的
setstate(flag):给流状态添加flag状态
rdstate():获取当前的流状态
一个经典的检测当前流状态的例子:
string s;
while(cin>>s,!cin.eof())
{
if(cin.bad())
{
throw runtime_error("IO Exception");
}
if(cin.fail())
{
cerr<<"bad";
cin.clear(istream::failbit);
continue;
}
}
6.输出缓冲区的管理:之前我们常常用endl来刷新缓冲区。现在可以用flush来刷新流,但是不在输出中添加任何字符,ends会在缓冲区中插入null字符,然后再刷新它。当需要刷新所有的输出时,可以使用unitbuf,这个操作符在每次执行完之后都刷新流。例如:
cout<<unitbuf<<”a”<<”b”<<nounitbuf;等价于cout<<”a”<<flush<<”b”<<flush;
tie函数可以将流绑定在一起,这个函数可以由istream和ostream调用,形参ostream指针类型。tie函数传递0时表示打破当前的捆绑。
7.fsteam文件流的使用:C++中文件采用的仍然是char类型,因此常常会用到将string转换成char类型的c_str()方法,因为我们可以先把文件名读入到string中,然后通过这个方法转化成C风格字符串传递给fstream对象。
8.定义文件流时可以指定文件,也可以不指定之后通过open方法来指定。std::ifstream a; a.open(“test”);和std::ifstream a(“test”);可以直接通过检查流对象是否有值来看当前的文件流是否有用。
如:
std::ifstream a("test.txt"); cout<<a<<endl;
if(!a) cout<<"no"<<endl;
需要注意的是当同一个文件流对象与不同的文件关联时,必须要先close()现在的文件,然后再打开另一个文件。
如果需要重用文件流读写多个文件,那么必须在读另一个文件之前调用clear()清除流状态。
9.读取文件中的内容时采用ifstream[输入文件流],写文件的时候采用ofstream[输出文件流]。因为写文件时是将内容写入到文件中,文件相当于接收输出。读文件时将文件中的内容读出来,文件相当于输入。
下面给出实例:
(1)读文件内容
/打开文件
std::ifstream rfile("file.txt");
if(!rfile)
{
std::cout<<"不可以打开文件"<<std::endl;
exit(1);
}
//读文件
char str[100];
rfile.getline(str,100);//读到'\n'终止
std::cout<<str<<std::endl;
(2)写文件内容
std::ofstream a("file.txt");
if(!a) cout<<"no"<<endl;
string wa("niihao");
a<<"niihaos";//或者 a<<wa;
10.文件都有自己的模式,一般有以下6中模式:
in: 表示在打开文件的时候读,ifstream默认模式
out:表示在打开文件时写,ofstrean默认,它打开时会自动清空当前文档中的内容
app:在每次写之前找到文件尾,ofstrean
ate:打开文件后立即找到文件尾(ifstream和ofstream都可以使用)
trunc:打开文件时清空已存在的文件流,ofstrean
binary:以二进制形式进行IO操作(ifstream和ofstream都可以使用)
当要打开模式的有效组合时,用|进行连接。
11.字符串流sstream:直接和内容中的string类型绑定.str()和str(s)方法返回当前的string值。
输入流:
istringstream stream(s);
stream>>wa;
cout<<wa;
输出流:
string m;
ostringstream stream1(m);
stream1<<"woo";
cout<<stream1.str();
12.输入流input是从流中传递数据给外面的参数,所以用的是>>这样的符号;输出流output是从外面获取数据到输出流中,所以用的是<<这样的符号。
(9) 第九章 顺序容器
1.顺序容器与关联容器的区别在于顺序容器中的元素按照顺序排放,关联容器中的元素按照key的顺序排放。
2.顺序容器有三种,分别是vector,list和deque(双端队列),有三种建立在顺序容器之上的适配器,分别是stack,queue和priority_queue。适配器就是封装在顺序容器之上的结构,它们实际的底层结构是顺序容器,但是不能直接调用顺序容器的方法和属性。list和deque可以在头部插入数据,因此可以用push_front操作,vector没有这个操作。vector和deque可以快速随机访问到数据,所以可以用[]下标操作,list不行。
3.顺序容器的初始化:
三种顺序容器初始化的方式有5种,这和关联容器有些许区别:
C<T> c;
C<T> c(c1); //用c1去初始化c容器,要求c1与c的容器类型相同,且存放相同类型的元素。
C<T> c(n,t); //用n个t去初始化当前容器,关联容器不支持
C<T> c(iter,iend); //用迭代器范围去初始化当前容器,此时不要求迭代器指向的对象与当前顺序容器类型完全相同,即指可以初始化与当前容器类型不同,且存放类型也不相同的容器。只要这两种容器之间可以相互兼容。
C<T> c(n) ;//用n个T类型的初始化值初始化当前容器,关联容器不支持
由于容器元素必须要求支持赋值运算和元素对象可以复制的操作,所以基本上所有的库类型都可以当做是容器元素类型,但是引用和I/O操作这两种操作中,引用不支持赋值,I/O不支持赋值或复制操作,所以不能用作容器元素类型。
4.顺序容器迭代器的操作:
顺序容器的迭代器可以执行的运算操作有:
*iter //解引用
iter->mem
++iter //自增
iter++
--iter //自减
iter--
iter1 == iter2 //判断是否相等
iter1 != iter2
另外由于vector和deque是连续存储的,所以可以执行一些特殊的操作如下:
iter + n
iter - n
iter += iter2
iter -= iter2
iter -iter2
iter > | <|>=|<= iter2
我们知道迭代器的范围是左闭右开的,即[a,b),就是说b这个迭代器指向的是迭代器范围最后一个元素的下一位,无论是在前面用迭代器赋值时,还是end()函数返回的迭代器值。
在执行完erase函数的时候迭代器常常会失效,这个时候需要一些有效地措施。这里涉及到erase函数返回值。
5.顺序容器的操作:
下面首先给出顺序容器中一些操作的类型:
vector<int>::size_type a = c.size(); //存放容器大小的类型
vector<int>::iterator b = c.begin(); //迭代器
vector<int>::const_iterator b = c.begin(); //const迭代器,不能改变迭代器指向的内容
vector<int>::reverse_iterator b = c.rbegin(); //按逆序寻址的迭代器,只能和rbegin(),rend()配合使用
vector<int>::const_reverse_iterator b = c.begin();
vector<int>::difference_type m = iter1-iter2; //由于涉及到两个迭代器相减,所以只有vector和deque才能有这个类型,list这么使用报错
value_type //元素类型
reference //value_type&
const_reference
顺序容器的操作函数:
(1)返回迭代器
begin()
end()
rbegin() // 指向容器最后一个元素
rend() //指向容器第一个元素前面的元素
(2)插入
push_back(t) //往后插入一个元素
push_front(t) //在前部插入一个元素,只有list和deque适用
insert(p,t) //通用方法,在迭代器p之前插入t,返回指向新添加元素的迭代器,其他都返回void
insert(p,n,t) //在迭代器p之前插入n个t
insert(p,b,e) //在迭代器p之前插入[b,e)的内容,不包括e的内容
(3)删除
erase(p) //删除p所指向的元素,返回被删除元素后面的元素的迭代器
erase(b,e) //删除[b,e)的元素,返回被删除段后面的元素的迭代器
clear() //删除容器的所有内容,返回void
pop_back() //删除最后一个元素
pop_front() //删除最前面一个元素,只有list和deque适用
由于删除元素之后可能导致迭代器失效,通常采用的方式比如说将迭代器iter=c.erase(iter);然后继续之后的方法。
(4)检测容器状态
size() //返回容器元素个数
max_size() //返回容器最大能容纳的元素个数
empty() //返回当前容器的状态是否为空
resize(n) //调整容器的大小使其能容纳n个元素,resize时可能会删除一些容器中的元素
resize(n,t) // 调整容器的大小使其能容纳n个值为t的元素
vector容器在内存处理上有预留空间,它有两个函数capacity()返回当前容器可以存储的最大空间,reserve(t)告诉容器预留t个元素的存储空间。这两个函数时vector特有的,list和deque都不能使用。
(5)访问容器中的元素,一种是迭代器解引用,一种是下面的方法
c[n] //只有vector和deque才能使用,因此是随机访问
c.at(n) //返回下标为n个元素,只有vector和deque才能使用
c.back() //返回最后一个元素的引用
c.front() //返回第一个元素的引用
(6)容器的比较与置换:
所有的容器都支持关系操作符来对两个容器的内容进行比较,但是前提是容器类型和容器中元素类型都必须要相同。
c1 = c2; //删除c1的所有元素,将c2的内容赋给c1
c1.swap(c2); //交换c1和c2中的元素,节省成本
c1.assign(b,e) //重置c1中的元素,将[b,e)中的内容赋给c1
c1.assign(n,t) //重置c1中的元素为n个t
(10) 第十章 关联容器
1.关联容器通过key存储和读取元素,而顺序容器通过位置存储和访问元素。一共大致有4中关联容器,大致分为两类,一类是键值只能出现一次的map、set,即键值在容器中唯一。另一种是键值可以出现多次的multimap和multiset。其中map以key_value的方式组织,set仅包含一个key,可以有效地支持key是否存在的查询。
2.在utility头文件中存在一种标准库类型pair。pair包含两个数据值,pair可以有以下操作:
pair<t1,t2> p1; //创建一个元素分别是t1,t2类型的空pair对象
pair<t1,t2> p1(v1,v2); //创建一个pair对象,用v1,v2初始化
make_pair(v1,v2); //用v1,v2初始化pair对象,返回一个pair对象
p1<p2; //pair之间的小于运算
p1 == p2; //需要pair的first,second都相等才返回true
p.first
p.second //pair的数据成员都是public的,所以可以直接访问,不需要通过函数访问
Eg: pair<string,int> word_count;
Eg2:
pair<string,string> next_auth;
string first,last;
while(cin>>first>>last){
next_auth = make_pair(first,last);
}
3.关联容器通过key访问数据,因此不提供front,back,push_front,push_back,pop_front,pop_back等操作。它和顺序容器公共的构造函数包括c<t> c; c<t> c1(c2); c<t> c(b,e);关联容器的erase函数返回void类型。在迭代遍历关联容器时,可确保键的顺序访问元素,与元素在容器中的存放位置无关。
4.map类型的键必须定义<操作符,且<操作符需要严格的弱排序。如果默认的键类型不存在<操作,那么需要自定义操作符函数。
map<k,v>::key_type //键类型
map<k,v>::mapped_type //关联值类型
map<k,v>::value_type //pair类型包括键和值一起,键成员不能修改,值成员可以修改。键是const map<k,v>::key_type类型,value是map<k,v>::mapped_type
对map迭代器解引用可以得到pair对象[value_type类型]。可以通过pair的first和second获得key和value。其中key是const类型的,不可修改。
map的插入操作有两种实现方式,分别是调用insert函数和先用下标获取元素再给获取的元素赋值。
(1)先获取再赋值:
map<string,int> word_count;
word_count[“anna”] = 1;
使用下标访问不存在的元素将导致在map容器中添加一个新元素,键为下标值。