#
31、动态空间的释放(P119)
动态空间的释放使用delete [] pia;(其中pia为指向动态分配的数组的第一个元素的指针)。在关键字delete和指针之间的方括号对是必不可少的:它告诉编译器该指针指向的是自由存储区中的数组,而并非单个对象。
如果遗漏了空方括号对,这是一个编译器无法发现的错误,将导致程序在运行时出错。
使用std::string后则会自动进行释放,无需delete!
32、c_str返回的数组并不保证一定是有效的……
std::string st1("I am a string object!");
//error C2440: “初始化”: 无法从“const char *”转换为“char *”
//char *str = st1.c_str();
const char *cstr = st1.c_str();
std::cout<<cstr<<std::endl;
c_str返回的数组并不保证一定是有效的,接下来对st1的操作有可能会改变st1的值,使刚才返回的数组失效。如程序需要持续访问该数据,则应该复制c_str函数返回的数组。
33、指向函数的指针
详见《指向函数的指针的一点理解》
34、容器初始化
在大多数的程序中,使用默认构造函数能达到最佳运行时性能,并且使容器更容易使用。
35、容器头文件
#include <vector>
#include <list>
#include <deque> //双端队列“double-ended queue”,发音为“deck”
标准库定义了以上三种顺序容器类型。
36、容器的容器
必须用空格隔开两个相邻的>符号,以示这是两个分开的符号,否则,系统会认为>>是单个符号,为右移操作符,并导致编译时错误。
int m, n;
m = 5;
n = 3;
cout << "Print a line!" << endl;
vector<string> lines(n, " I_am_a_PC_! ");
for (vector<string>::iterator siter = lines.begin(); siter != lines.end(); ++siter) {
cout << *siter;
}
cout << endl;
cout << "Print a paragraph!" << endl;
vector<vector<string> > paragraph(m, lines);
for (vector<vector<string> >::iterator piter = paragraph.begin(); piter
!= paragraph.end(); ++piter) {
for (vector<string>::iterator siter = (*piter).begin(); siter
!= (*piter).end(); ++siter) {
cout << *siter << ends;
}
cout << endl;
}
37、迭代器范围(P269)
C++定义的容器类型中,只有vector和deque容器提供下面两种重要的运算集合:迭代器算术运算,以及使用除了==和!=之外的关系操作符来比较两个迭代器(==和!=这两种关系运算适用于所有容器)。
list容器的迭代器既不支持算术运算(减法或加法),也不支持关系运算(<=,<,>=,>)它只提供前置和后置的自增、自减以及相同(不等)运算。
38、容器元素都是副本
在容器中添加元素时,系统是将元素值复制到容器里的。类似地,使用一段元素初始化新容器时,新容器存放的是原始元素的副本。被复制的原始值与新容器中的元素各不相关,此后容器内元素值发生变化时,被复制的原值不会受到影响,反之亦然。
39、下标操作和.at(n)的区别
vector<string> svec; //empty vector;
cout << svec[0]; //run-time error:There are no elements in svec!
cout << svec.at(0); //throws out_of_range exception
40、容器的赋值(P283)
assign操作首先删除容器中所有的元素,然后将其参数所指定的新元素插入到该容器中。与复制容器元素的构造函数一样,如果两个容器类型相同,其元素类型也相同,就可以使用赋值操作符(=)将一个容器赋值给另一个容器。如果在不同(或相同)类型的容器内,元素类型不相同但是相互兼容,则其赋值运算必须使用assign函数。例如,可通过assign操作实现将vector容器中一段char*类型的元素赋给string类型的list容器。
由于assign操作首先删除容器中原来存储的所有元素,因此,传递给assign函数的迭代器不能指向调用该函数的容器内的元素。
准确地讲,本文所涉及的内容是C++中较难理解的,本文的目的不是在于将它们解释清楚,因为这需要你循序渐进地做很多练习才可以。看下面一个例子:
int (*func(bool real))(int, int)
你觉得它的返回值是什么?
这里就涉及到了如何理解指向函数的指针的问题了。一些来自C++教材的建议是从里向外解读这个表达式,这里所谓的里面就是func(bool real),那么剩下的部分就是所谓的返回值了?有点生硬吧。下面就让我们循序渐进地看看如何理解更好?
为什么会对这个表达式的返回值产生疑问?
要解决问题通常需要找出问题所在,这里是基于这样一种思维定势,那就是我们通常习惯于这样一种声明变量的方式:
int a;
这里我们声明a是一个int类型的变量。而对于返回值,我们通常也是采用类似的方式,如一个返回值为int类型的函数通常可以以下面的方式进行声明:
int func([params]);
因此我们惯性地认为返回值就是最左侧的一个类型名,虽然这通常是对的,但是针对上面的那个例子则显得十分尴尬。
让我们看看一个指向函数的指针的声明式:
int (*pCompare)(int, int);
这个指针的名字就是pCompare,令人奇怪的是pCompare并不是在整个声明式的最右边,类型也肯定不是int,而是一个复杂的表达式。让我们用typedef来声明就会发现typedef的使用也不太一样。
typedef int (*PF)(int, int);
我们发现跟惯用的typedef *** ???;的方式也截然不同,在上面这个typedef过后,整个表达式可以被简化成:
PF pCompare;
现在我们似乎就一见如故了,现在的表达式看起来中规中矩,普通的声明都是类型名加变量名完成声明,而函数指针的声明则是在一个表达式中一个固定的位置进行声明。
int (* )(int, int);
在上文中划线的部分即为声明的部分,也就是这点不同让我们逐渐迷失了方向。
现在让我们写一个返回指向函数的指针的函数,也就是返回值是PF的函数,这就像我们从返回int类型的变量到返回int类型值的函数一样,因此使用以下方式即可:
PF func([params]);
现在让我们扩展PF,将它还原,也就是把右侧的func([params])部分移到那个横线的位置上。现在我们就可以很轻松地理解本文开头的那个函数,原来是返回值为int (*)(int, int)的函数
int (*func(bool real))(int, int)
以上划线的部分也就是一个函数扣除返回值的部分。也就等价于
PF func(bool real)
至此你应该能够分析更加复杂的表达式了。
下面的示例旨在帮助理解本文:
/*
* main.cc
*
* Created on: 2009-2-1
* Author: Volnet
*/
#include <stdlib.h>
#include <iostream>
using std::cout;
using std::endl;
int myCompare1(
int a, int b,
int (*Compare)(int, int));
int realCompare(int a, int b);
int fakeCompare(int a, int b);
typedef int (*PF)(int, int);
int myCompare2(
int a, int b,
PF Compare);
PF getAPointerFunc1(bool real);
int (*getAPointerFunc2(bool real))(int, int);
int main(void){
int typeDeclared;
typeDeclared = 1;
//PF pCompare;
int (*pCompare)(int, int);
if(pCompare == NULL)
cout<<"pCompare == NULL"<<endl;
else
{
cout<<"pCompare != NULL"<<" pComapre = "<<pCompare<<endl;
}
cout<<"Compare the pointer function."<<endl;
cout<<"The compare result is : "<<
myCompare1(6, 5, realCompare)<<endl;
cout<<"It's the same to invoke realCompare & *realCompare : "<<
myCompare1(6, 5, *realCompare)<<endl;
cout<<"Using the typedef to predigest definition : "<<
myCompare2(8, 7, realCompare)<<endl;
cout<<"Return a pointer from a function : "<<
myCompare2(10, 20, getAPointerFunc1(true))<<endl;
cout<<"Return a pointer from a function : "<<
myCompare2(20, 30, getAPointerFunc2(false))<<endl;
return EXIT_SUCCESS;
}
int myCompare1(
int a, int b,
int (*Compare)(int, int)){
return Compare(a, b);
}
int realCompare(int a, int b){
cout<<"The realCompare has be invoked."<<endl;
if(a == b)
return 0;
if(a < b)
return -1;
else
return 1;
}
int fakeCompare(int a, int b){
cout<<"The fackCompare has be invoked."<<endl;
return 200;
}
int myCompare2(
int a, int b,
PF Compare){
return Compare(a, b);
}
PF getAPointerFunc1(bool real){
if(real)
return realCompare;
else return fakeCompare;
}
int (*getAPointerFunc2(bool real))(int, int){
if(real)
return realCompare;
else return fakeCompare;
}
21、vector的动态增长优于预先分配内存。
使用vector的时候最好动态地添加元素。它不同于C和Java或其他语言的数据类型,为了达到连续性,更有效的方法是先初始化一个空vector对象,然后再动态添加元素,而不是预先分配内存。
22、vector值初始化
内置->0
有默认构造->调用默认构造
无默认构造,有其他构造->程序员手动提供初始值
无默认构造,也无其他构造->标准库产生一个带初值的对象
23、数组下标的类型
C++中,数组下标的正确类型是size_t而不是int,size_t是一个与机器相关的unsigned类型。
24、在声明指针的时候,可以用空格将符号*与其后的标识符分隔开来,string *ps与string* ps都是可以的,但后者容易产生误解,如:
string* ps1,ps2; //ps1是指针,而ps2是一个string对象
也就是说,人们可能误把string和string*当作两个类型,或者说string*被当作一种新类型来看待,但这是错的!
25、一个有效的指针必然是以下三种状态之一:
- 保存特定的对象的地址;
- 指向某个对象后面的另一对象;
- 或者是0值。表明它不指向任何对象。
其中int *pi=0;与int *pi;是不同的。前者是初始化指针指向0地址的对象(即为NULL)(pi initialized to address to no object),后者却是未初始化的(ok, but dangerous, pi is uninitialized)。
编译器可以检测出0值的指针,程序可判断该指针并未指向一个对象,而未初始化的指针的使用标准并未定义,对大多数编译器来说,如果使用未初始化的指针会将指针中存放的不确定值视为地址,然后操纵该内存地址中存放的位内容,使用未初始化的指针相当于操纵这个不确定的地址中存储的基础数据,因此对未初始化的指针进行解引用时,通常会导致程序崩溃。
26、void*指针
void*指针只支持几种有限的操作:
- 与另一个指针进行比较;
- 向函数传递void*指针或从函数返回void*指针;
- 给另一个void*指针赋值。
不允许使用void*指针操纵它所指向的对象。
27、指针和引用的比较(P105)
虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别。第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。
28、指针与typedef(P112)
const放在类型前和放在类型后都可以表示同样的意思:
const string s1;
string const s2;
s1和s2均表示常量字符串对象。
但因此就导致了下面的句子可能产生误解:
typedef string *pstring;
const pstring cstr;
容易错把typedef当成文本扩展而产生下面的理解:
const string *cstr; //这并非上面例子的正确意思!(错误)
应该从声明的句子看,也就是说只看const pstring cstr;,在这里pstring是一种指针类型,const修饰的是这个类型,因此正确的理解应该是:
string *const cstr;
而const pstring cstr;其实可以表示为pstring const cstr;,这样的写法则不容易产生误解。从右向左阅读的意思就是:cstr是const pstring类型,即指向string对象的const指针。
29、创建动态数组(注意点见代码注释)
const char *cp1 = "some value";
char *cp2 = "other value";
int *piArray1 = new int[10]; //内置类型没有初始化
int *piArray2 = new int[10](); //内置类型需要加空圆括号,对数组元素进行初始化
std::string *psArray1 = new std::string[10]; //默认构造函数初始化
std::cout<<"----------"<<std::endl
<<"*cp1\t\t:"<<*cp1<<std::endl
<<"*cp2\t\t:"<<*cp2<<std::endl
<<"*piArray1\t:"<<*piArray1<<std::endl
<<"*piArray2\t:"<<*piArray2<<std::endl
<<"*psArray1\t:"<<*psArray1<<std::endl
<<"----------"<<std::endl;
但是下面的结果却与概念上的不同:
////Visual Studio & MS VC++
//----------
//*cp1 :s
//*cp2 :o
//*piArray1 :-842150451
//*piArray2 :0
//*psArray1 :
//----------
////Eclipse&G++
//----------
//*cp1 :s
//*cp2 :o
//*piArray1 :4064608
//*piArray2 :4064560
//*psArray1 :
//----------
看来不同的编译器对此的定义还是有所不同,注意看*piArray2的值,按照说明应该是初始化为0,但这里却仍然表现出与*piArray1一样的值,说明并没有发生初始化。
对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。
30、const对象的动态数组
//P118
//error:uninitialized const array
const int *pciArray1 = new const int[10];
//ok:value-initialized const array
const int *pciArray2 = new const int[10]();
std::cout<<*pciArray1<<std::endl;
std::cout<<*pciArray2<<std::endl;
上面的示例的注释来自书中,但在VC++编译器和G++编译器下却不同,具体表现为:
- VC++:编译正确,第一句输出随机地址的值,第二句输出初始化的0(其中按照“标准”第一种因为未向const变量初始化,应该无法通过编译,但这里可以)
- G++:编译错误,第一句的错误信息为“uninitialized const in `new' of `const int'”,但第二句按照标准应该输出0的,这里却输出了随机地址的值。
看来两个编译器对这一问题的看法不太一致。
11、枚举
//enum
enum HttpVerbs { Head, Post, Get, Delete };
HttpVerbs current_verbs = Post;
std::cout<<"Current Verbs = "<<current_verbs<<std::endl;
//error C2440: “=”: 无法从“int”转换为“HttpVerbs”
//current_verbs = 3;
current_verbs = (HttpVerbs)2;
std::cout<<"Current Verbs = "<<current_verbs<<std::endl;
HttpVerbs copy_verbs = current_verbs;
std::cout<<"Copy Verbs = "<<copy_verbs<<std::endl;
HttpVerbs future_verbs = (HttpVerbs)((current_verbs + 1)%sizeof(HttpVerbs));
std::cout<<"Future Verbs = "<<future_verbs<<std::endl;
std::cout<<"HttpVerbs Size![by sizeof(HttpVerbs)] = "<<sizeof(HttpVerbs)<<std::endl;
输出:
Current Verbs = 1
Current Verbs = 2
Copy Verbs = 2
Future Verbs = 3
HttpVerbs Size![by sizeof(HttpVerbs)] = 4
12、类,成员变量初始化
定义变量和定义数据成员存在着非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。类不是在类定义里定义数据成员时初始化数据成员,而是通过成为构造函数的特殊成员函数控制初始化。
class MyClass1
{
public:
int GetMyValue();
void SetMyValue(int value);
private:
//error C2864: “MyClass1::myValue”: 只有静态常量整型数据成员才可以在类中初始化
int myValue = 3; //只需修改为int myValue;即可
};
13、struct关键字
C++支持另一个关键字struct,它也可以定义类类型。struct关键字是从C语言中继承过来的。
如果使用class关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为private;如果使用struct关键字,那么这些成员都是public,除非有其他特殊的声明,如添加了private才为private,否则都是public,因此没必要添加public关键字。
用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct的成员为public,而class的成员为private。
14、预编译头文件
一、什么是预编译头文件?
预编译头文件物理上与通常的的.obj文件是一样的,但编译入预编译头的.h,.c,.cpp文件在整个编译过程中,只编译一次,如预编译头所涉及的部分不发生改变的话,在随后的编译过程中此部分不重新进行编译。进而大大提高编译速度,并便于对头文件进行管理,也有助于杜绝重复包含问题。
VC++程序一般包含的头文件都比较复杂,如果每次都逐行分析可能会花很多时间,所以VC++默认设置是第一次编译时分析所有头文件,生成.pch文件,这个文件很大,但以后每次编译时就可以节省很多时间。如果删除了这个文件,下次编译时VC++会自动生成它。
二、什么时候使用预编译头?
当大多.c或.cpp文件都需要相同的头文件时。
当某些代码被大量重复使用时。
当导入某些不同库都有实现的函数,并产生混乱时。
15 、在头文件中必须总是使用完全限定的标准库名字。
因为预处理器会将头文件复制到使用它的任何地方,两种可能,一种是如果在头文件中使用using,会使相关代码不论是否需要该using都必将放置一个using,另一种是,假设有另一个库可能也包含了相应的方法如有方法std::cout以及my::cout,如果使用了using,有可能导致被引入的程序偏离原本的使用意图,或者导致编译错误。
16、字符串字面值和标准库string不是同一种类型
因为历史原因以及为了与C语言兼容,字符串字面值与标准库string类型不是同一种类型。这一点很容易引起混乱,编程时一定要注意区分字符串字面值和string数据类型的使用,这很重要。
17、getline函数输入的时候丢弃末尾的换行符,并将整行返回,而且不丢弃第一个换行符,也就是即便你一开始就输入了换行符,它仍然会返回,只不过返回的是一个空字符串而已。
编写程序实现从标准输入每次读取一行文本。然后改写程序,每次读入一个单词!
//std::cout<<"getline:<<std::endl;
WriteLine("getLine");
WriteLine("P72 编);
using std::string;
WriteLine("每(输"NEXT\"进");
string str;
while(std::getline(std::cin, str))
{
if(str == "NEXT")
break;
std::cout<<str<<std::endl;
}
WriteLine("每输"NEXT\"进");
while(std::getline(std::cin, str))
{
if(str == "NEXT")
break;
//从第0个开始查找空白
static const std::basic_string<char>::size_type npos = (std::basic_string<char>::size_type)-1;
std::basic_string<char>::size_type firstIndexOfEmpty = str.find_first_of(" ", 0);
if(firstIndexOfEmpty != npos)
std::cout<<str.substr(0, firstIndexOfEmpty)<<std::endl;
else
std::cout<<str<<endl;
}
其中WriteLine函数:
void WriteLine(std::string str)
{
std::cout<<str<<std::endl;
}
18、std::string对象的加法
如果一串字符串和string对象混合相加,则要求+操作符左右操作数必须至少有一个是string类型的。
推论:一串字符串和string对象混合相加,前两个操作数中至少有一个是string对象。
std::string str2 = str1 + "this" + " that" + " those";
std::cout << str2 << std::endl;
//error C2110: “+”: 不能添加两个指针
std::string str3 = "this" + " that" + str1 + " those";
std::cout << str3 << std::endl;
19、C标准库头文件和C++版本
C++标准库也包括C标准库,命名上则在C标准库的名字前加一个c并省去后缀.h,比如C标准库中的ctype.h,在C++中就有相应的标准库cctype(注意没有.h)。C++和C标准库文件的内容是一样的,只是采用了更适合C++的形式。而且通常cname头文件中定义的名字都定义在命名空间std内,而.h版本的名字却不是这样。
20、关于中文编码的相关问题
我们知道大部分的编译器以及解决方案都由外国人发明创造,特别是美国人。因此很多程序默认不支持中文。虽然随着Unicode的普及这部分问题得到了很大的改善(比如C#就可以完美地支持中文),但是类似C++这样的语言,仍然面临着中文编码的问题。关于编码,有一篇值得推荐的文章:地址1(备用地址)下载后打印(docx,50.7KB)查找更多
1、标识符命名规则!
标识符不能包含两个连续的下划线,也不能以下划线开头后面紧跟一个大写字母。有些标识符(在函数外定义的标识符)不能以下划线开头。
但是在G++编译器和VC编译器下,二者均可正确编译!
2、跨平台编译程序!
这里不是要讲解如何跨平台编译程序,也不是告诉你如何更好地编写通用平台的程序规则,那可能涉及到很多的宏定义以及硬件相关特性。这里仅为使用示例代码提供一种精简的方式。
用Eclipse+MinGW的方式默认会很精简,所以把它当作一种目标!
用Visual Studio 2008创建的程序会让你引入预编译头stdafx.h(这通常发生在使用Visual Studio创建Win32控制台应用程序,并直接点击“完成”后),这将导致你无法将在Eclipse上编写的程序直接运行在Visual Studio上。这时你应该通过修改项目属性来获得这种精简的方式:(选择项目,右键属性,选择配置属性->C/C++->预编译头->创建/使用预编译头,选择“不使用预编译头”->“确定”后再次编译即可!)
3、变量命名习题
//测试变量命名!
//error C2632: “int”后面的“double”非法
//int double = 3.14159;
//-------------------------------------------------
char _='a';
std::cout<<_<<std::endl;
//-------------------------------------------------
//warning C4091: “”: 没有声明变量时忽略“bool”的左侧
//error C2059: 语法错误: “-”
//bool catch-22;
//-------------------------------------------------
//error C2059: 语法错误: “数字上的错误后缀”
//char 1_or_2 = '1';
//-------------------------------------------------
float Float=3.14f;
std::cout<<Float<<std::endl;
4、在C++中,“初始化不是赋值”
初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
int ival(1024); //直接初始化
int ival = 1024; //复制初始化
直接初始化语法更灵活,效率更高!
对内置类型来说,复制初始化和直接初始化几乎没有差别。
对类类型来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。
例如:
也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定:
std::string all_nines(10, ‘9’); //all_nines = “9999999999”;
本例中,初始化all_nines的唯一方法是直接初始化。有多个初始化式时不能使用复制初始化。(V注:这里的初始化式即为构造函数的多个重载;这里所谓的“不能使用”应该是“功能有所不及”的意思!)
5、变量初始化规则
使用未初始化的变量经常导致错误,而且十分隐蔽。问题出在未初始化的变量事实上都有一个值。编译器把该变量放到内存中的某个位置,而把这个位置的无论哪种位模式都当成是变量初始的状态。当被解释成整型值时,任何位模式都是合法的值——虽然这个值不可能是程序员想要的。因为这个值合法,所以使用它也不可能导致程序崩溃。可能的结果是导致程序错误执行和/或错误计算。
//在Eclipse中运行没有出现错误!
//在Visual Studio中运行出现运行时错误!
int ival; //没有初始化!
std::cout<<ival<<std::endl;
6、声明和定义
为了能让多个文件访问相同的变量,C++区分了声明和定义。简单地说就是可以用extern关键字来声明,任何有分配内存行为的声明都是定义。定义也是声明。声明:标明变量的类型和名字;定义:为变量分配存储空间,还可以为变量指定初始值。
举例说明:
extern double pi; //声明
double pi; //定义,声明了pi同时定义了pi
extern double pi = 3.14159; //定义,因为它为pi分配了初值。只有当该extern语句
位于函数外部的时候才允许使用初始化式,否则将导致编译错误。
7、变量的隐藏:
std::string s1 = "I am a std::string!";
std::cout<<s1<<std::endl;
for(int s1=3; s1!=0; --s1)
std::cout<<"I am a number(int):"<<s1<<std::endl;
提示:在Visual Studio 2008中使用std::string定义一个变量,再通过std::cout将其输出,将会得到“error C2679: 二进制“<<”: 没有找到接受“std::string”类型的右操作数的运算符(或没有可接受的转换)”错误信息,这时要检查头文件中是否包含#include <string>。而在Eclipse中则不用如此设置(具体看编译器版本)。这与标准库实现的具体细节有关,在MSVC中,它在文件Program Files (x86)\Microsoft Visual Studio 9.0\VC\include\string中被实现,在GNU中,它在base_string.h中被实现。在使用std::string时,总是包含#include <string>是一个好习惯!
8、const对象默认为文件的局部变量
一般声明变量后可以在其它文件中通过extern关键字声明并使用该变量:
//文件1:
int counter;
//文件2:
extern int counter;
++counter;
但是如果是const则无法访问。可以通过显式指定extern关键字使其成为全局可访问对象:
//文件1:
extern const int bufSize = getBufSize();
//文件2:
extern count int bufSize;
//……使用bufSize
注解:非const变量默认为extern。要使const变量能够在其他的文件中访问,必须显式地指定它为extern。
9、引用
int ival = 1024;
int &refVal = ival;
当引用初始化后,只要该引用存在,就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。
也正因为如此,所以引用比指针的优势就在于:引用不可以在方法中篡改,这使得方法变量变得安全了。
10、const引用
const int ival = 1024;
const int &refVal = ival;
这里我们要求左侧的类型是一致的,包括const!
非const引用只能绑定到与该引用同类型的对象。
const引用则可以绑定到不同但相关的类型的对象或绑定到右值。(具体示例详见C++Primer v4 P52)
例如:
//错误代码
double dval = 3.14;
const int &ri = dval;
编译器会将这些代码转换成如以下形式的编码:
int temp = dval;
const int &ri = temp;
如果ri不是const,那么可以给ri赋一新值。这样做不会修改dval,而是修改了temp。期望对ri的赋值会修改dval的程序员会发现dval并没有被修改。仅允许const引用绑定到需要临时使用的值完全避免了这个问题,因为const引用是只读的。
但是如下代码可以执行:
int ival = 1024;
const int &refVal = ival;
++ival;
//++refVal; //error C3892: “refVal” 不能给常量赋值
std::cout<<"ival="<<ival<<"\trefVal="<<refVal<<std::endl;
输出:ival=1025 refVal=1025
const double dval = 3.14;
const int &ri = (int)dval;
std::cout<<ri<<std::endl;
输出:3
以上步骤基本上没有啥技术含量(一点都没有噢,只为记录一下,图片也漂亮点),注意到最后这个黑色的命令行,在安装结束之后会出现这么个命令行,其中会问你是否已经安装过MinGW了?在回答y之后,要求输入MinGW的地址,注意,在资源管理器里面我们都是用“\”来代表路径层级的分层的,在这里是用“/”(方向不一样)。另外地址中不允许带有空格。这一点在《如何安装MinGW》一文中已经有提到。
MSYS(Minimal GNU(POSIX)system on Windows)
由于本文的主角是它,我们先来看看它是什么?从名字的全称我们可以看出它是一个小型的GNU环境。MSYS在windows下模拟了一个类unix的终端,它只提供了MinGW的用户载入环境,Cygwin在windows下模拟了一个linux环境,它们带有一些unix终端下常用的工具,如ls、tail、tar,其实它们都是相应unix/linux工具的windows版,而且它们的环境会继承windows的一些系统变量,如path,如果windows下装有ruby、rails,在它们的环境里同样都可以直接运行。
Cygwin(基于GPL licensed协议)
Cygwin并不是GNU,它只是实现了许多Windows API中没有的Unix风格的调用(如fork,spawn,signals,select,sockets等),并将它们封装在Cygwin.dll中,让Windows系统能够模拟出Unix的调用,进而直接使用Unix上的交叉编译器来生成可以在windows平台上运行的工具集。以这些移植到windows平台上的开发工具为基础,cygnus又逐步把其他的工具(几乎不需要对源代码进行修改,只需要修改他们的配置脚本)软件移植到windows上来。这样,在windows平台上运行bash和开发工具、用户工具,感觉好像在unix上工作。
MinGW(Minimalist GNU For Windows)
主要由GNU binary utilities、GCC和GDB组成。同时还包括一些必要的库,例如libc(C Runtime),及专门用于Win32环境的API接口库。如果你想学习linux环境下的编程,而又不想装linux,那你就装一个MinGW吧。
它与Cygwin实现了相同的梦想,也是为了实现在Windows上能够运行Unix上的工具。但与之不同的是它采用的是Windows C类库(mscvrt)而不是Cygwin采用的GNU C运行时类库。同时也因为两个运行时类库的端口不同而导致两者有诸多区别。但是却因为采用了直接支持Windows环境的Windows C运行时类库,它也给CDT(C/C++ Development Toolkit)提供了最好的支持。同时,它避免了像Cygwin使用了GPL协议。
C/C++ Development Toolkit(CDT)
C/C++ Development Toolkit(CDT)是基于Eclipse特征的,为使用C/C++编程语言,提供创建、编辑、导航、生成(build)和调试项目的一个集合。
它不包含必要的转换C/C++代码的编译器和调试器来执行和调试程序,但是它提供了一个允许这些工具集成在一个相容方式下协作的框架。它允许你根据你的项目需求来混合和匹配这些工具。
通常,商业发行的CDT包括了必要的工具集。如果你没有,那么最基本的CDT提供了综合的GNU工具来生成和调试。他们通常指Cygwin和MinGW等。
做了三个简单的概念介绍后,我们知道了它们各自的一些特征。它们都不是像Visual Studio这样可以拿来直接就用的软件,它们的使用需要一些必要的配置。这也是社区文化的一种体现。
以上的安装步骤最后一步出现的命令行模式,确实是大部分安装程序中不常出现的,那么它究竟为我们做了什么呢?带着这样的疑问,我们滚动到这副截图来看看它的内容。从内容中我们可以看到在安装完毕之后我们会被要求输入MinGW的所在目录,根据文档中的说明,我们可以将MinGW放在除了MSYS目录的任何的一个目录中(也就是如图D:\GNU\MSYS位置不能放),至于放了之后会怎样,笔者也不知晓,望知晓者可以告知在下,定当感激不尽。
之后它会根据我们给出的路径在MSYS目录下的etc文件夹(如本例中的D:\GNU\MSYS\1.0\etc)内找到fstab文件并进行编辑(会在稍后描述)。然后检查我们给出的路径下的一些文件。从图中我们可以看到我们缺少了一个非常重要的make.exe文件,这个文件其实存在,只是文件名不同,我们可以将其重命名为make.exe。(本例中,我们将已经安装的D:\GNU\MinGW\bin\mingw32-make.exe修改为D:\GNU\MinGW\bin\make.exe即可)
fstab文件
与fstab相同路径下的有个fstab.sample文件,这是一个示例文件。您可以仿造它进行一些设置。(如果您按照之前我们的安装步骤,并且中途没有出现偏差的话,那么通常您已经正确设置了fstab文件,如果因为其它原因您需要修改该文件,也可以参考该部分内容。)fstab文件将是dll初始化期间唯一被载入的文件。它的格式我们可以看到,是由一个物理路径+“空格/TAB制表格”+Mount_Point来维持的。它实现了一个路径映射的体系结构,以至于我们不必手动地搬动那些文件到正确的目录,也能够让我们正确地访问我们所需的文件。
下图为fstab.sample文件
下面我们打开fstab看看系统之前为我们设置了什么:
从本例子中我们发现了系统为我们做好了这样一个映射。将D:/gnu/mingw映射到了/mingw。
MSYS只是一个模拟的平台,我们除了让它跟MinGW实现互联互通,我们也能够让它跟系统上的其他程序互联互通。
通过运行D:\GNU\MSYS\1.0\msys.bat批处理文件,我们可以打开如上图所示的命令行窗口(它的功能基本上类似于运行了D:\GNU\MSYS\1.0\bin\sh --login -i语句)
执行的内容相当于运行了下面的批处理语句。
下面让我们打开一个word程序。我们在命令行下输入:
$ start '/d/Program\ Files/Microsoft\ Office/Office12/WINWORD' $@
语句(具体路径视您本机的word安装程序路径所定)
我本机的路径为
D:\Program Files\Microsoft Office\Office12\WINWORD.EXE
我们很容易看出它们二者之间的区别。下面我就针对这些区别做一些简要的解释。
首先我们看“D”是一个盘符,在这个映射里面,我们规定,盘符若为“D:\”我们就将其转化为“/d”,同时为了与Unix的使用习惯一致,在资源管理器中表示层次的“\”符号都变成了“/”,若遇到空格,我们则以“\ ”(\+空格)来表示。因此我们有了如上一个等价的路径转换。
复制粘帖
说到使用这样的命令行方式大家一定觉得还不是那么方便。因为这里不能够复制粘贴。但你错了,这里复制粘贴也很容易。
复制:选择你要复制的部分,然后鼠标点一下,就可以了。容易吧?
粘帖:只需用shift+鼠标左键,就可以直接将剪贴板内的文字粘帖进来了。
(更多办法请参考doc/msys/readme.rtf)
在Windows上使用gcc编译器,我们需要获取一些第三方的工具。MinGW是一个基于GNU规范的可以在Windows上编译的第三方工具。MinGW与著名的Cygwin的差别在于它使用Windows系统的C运行时(mscvrt)取代了GNU的C运行时。因为兼容层不是必须的,因此避免了Cygwin的GPL授权问题。
同时也因为它直接使用了Windows系统的C运行时,从而增强了利用它开发的程序在Windows系统上的兼容性。
1、首先到sourceforge.net(http://sourceforge.net/projects/mingw/)上下载最新的MinGW。如果您在安装的时候始终和互联网保持连接(这也是必须的),那么首先安装包会检测当前的版本是否为最新,如果不是,则会提示有更新。这时,您需要自己上网站上去下载最新的版本。
2、下载的最新版本安装的时候如下图解。基本上都是NEXT的操作,所以也没有什么可说明的。
3、值得注意的一点是,安装路径中最好不要有空格,因为在别的软件的引用中可能出现问题。类似:Program files的最好都不要出现。
至此,这个MinGW就已经安装完了,它是一组有用的工具集(这些工具将包含在$\bin目录下(其中 $代表您所选择的安装路径))。
摘要: 初入Emacs,还是要入乡随俗的,基于Unix的程序还是在键盘命令上下了很大的功夫,但这样的通用性也得到了极大的提高。下面是拷贝Emacs的快速指南的中译版本,只是记录一下,没有别的意思。呵呵提供一下Windows平台Emacs的安装方式:1、下载Emacs基于Windows的安装包,我是从ftp://ftp.keystealth.org/pub/gnu/gnu/emacs/windows/ema...
阅读全文
Eclipse(http://www.eclipse.org/)
Eclipse是一个由IBM公司牵头开发的一个自由软件,后来IBM公司为了让更多的公司积极参与进来而不至于让它们因为这是一个由IBM主导的软件而导致软件开发者们拒之门外,Eclipse的身份则转为由一个固定员工的组织所维护的非营利组织。它和所有的自由软件一样,被免费地提供给所有热爱开源事业的人们,通过集体的智慧将它进行完善。
Eclipse是一款跨平台的IDE,它既不是编译器,也不是简单的编辑器,它提供了一个开放的平台用于为各种各样的编译器,开源或者不开源的,提供一个能够共同操作的平台。由于它是一款基于Java虚拟机的应用软件,因此它同时也是一款跨平台的IDE。跨平台的特性让它的存在有了更有征服力的理由。众所周知,Linux是开源社区中的一颗璀璨夺目的明珠,以Linux为核心的开源软件组织也是数不胜数,无数人为之奋斗倾尽心血。但是作为开发人员,Windows平台上的Visual Studio一贯的平易近人(不是指价格上)让所有的程序员所称赞。就算是要搞Borland Delphi也有一款优秀的IDE在实时待命。但是在Linux上有啥?对于骨灰级的程序员,拿个Emacs甚至一个记事本就可以将编程进行到底。他们追求的无非就是换行和显色等漂亮点的效果,对于项目级的维护和便利并没有过高的要求,也许是一种习惯,也许是一种妥协。但是这一切因为Eclipse的出现而大大变样了,程序员从纷繁的makefile中解脱出来专心于逻辑代码的编写,很多自动化的组件出现更增加了程序的健壮性。虽然旧式的编码方式显得更专业,但还是一定程度上束缚了生产力的发展。
多说无益,反正IDE的出现总是预示着编程门槛的降低,随之而来带来的就是在该平台上的投入的人员越来越多而让这个平台能够接受越来越多的人来参与。这对这个产业绝对是“生产力大奖”的。
CDT(http://www.eclipse.org/cdt/)
CDT英文全称是C/C++ Developer Tools,CDT是为Eclipse平台提供集成开发环境的一个项目。我们知道将Eclipse+CDT就可以用于开发C++了,现在又说这CDT也是个IDE,这是怎么回事呢?事实是这样的,因为Eclipse是个开放的平台,所以它希望所有的开发者都能够参与进来。但是现在市面上流行的语言种类太多了,多得有点数不过来,有些甚至只有少数几个人自己会用(比如中文编程语言吧)。所以Eclipse公开了自己的部分接口让这些语言能够定义带有自己特征的部分功能。可以这么理解,Eclipse定义了IDE的共性部分,而类似CDT这样的则定义了IDE的个性部分。所以它是专门针对C/C++IDE的个性化组件。有了它,用Eclipse写C/C++代码才显得更有价值。
CDT的组件安装,则是将安装包解压后,直接覆盖到Eclipse的目录下,重新启动Eclipse即可。关于这点内容可能根据具体的版本会略有不同。当然最简单的方式是下载C/C++版本的Eclipse。
编译器(Complier)
每一个科班出身的程序员对这个词都耳熟能详,每个人对这个词都有自己的理解。但是真正接触过的程序员又有多少呢?来一段软件开发技术发展的简史,传说在很久以前,搞计算机的都是科学家,那时候大家刚刚从硬件电板上转到键盘编码的过程中来,用汇编+DOS似乎是一件很享受的事,但是那批科学家已经青春不在了。新来的年轻人又弄出了高级语言这种接近人类思维的语言,很快汇编就成为了专属领域的内容,能用高级语言的地方,没有人愿意让汇编有用武之地。很快汇编也渐渐淡出视线。然后就是图形化界面的出现,但这本身并没有改变人们使用编译器的方式。无非就是换成记事本去编写代码罢了。但是图形化的出现促使了图形化IDE的出现,人们发现编译事实上又是遵循一定规律的集合,这部分可以很容易地通过一些编程上的限制或者通过代码可以整理出人们期待的编译方式,于是很快makefile一类的事也变成人们可以淡忘的事了。现在人们使用许多优秀的IDE,编译不过只是按一下“编译”或者输入个快捷方式。那些难记的参数早已经忘到九霄云外了。由于编程的门槛在大大降低,所以是越来越多的程序员跟编译器无一面之缘了。我们称他们都是被惯坏的程序员,当然并不表示他们就怎么不好了,只是一个社会现象罢了。正如我们会用白米煮饭,并不需要我们去理解谷子的剥皮方式,也不需要去理解水稻的种植。我们只要懂得选择哪种米能煮出更好吃的米饭就可以了。当然了,这一点在Windows平台上显得更为常见,因为Windows是最普及也是很优秀的一款图形化的操作系统。但是在Unix上,计算机的发展还没有那么文明。这里的大多数程序员从水稻种植到磨成白米,样样都得会,不然连HelloWorld都整不出来。而且每次要Say Hello就得重头开始写makefile,手动编译等。虽然这看起来很酷,但是没有多少意思。
C/C++的编译器很多,比较常见的优秀作品有MS C++,GCC(G++),Borland C++等,其中在Windows平台上基本都用MS C++,而且MS C++也是截至我发稿时最接近ISO C++标准的编译器(据说已经实现了标准的98%以上),但是在Linux平台上,则多数是以GCC为主,其中GNU GCC Complier则是其中的佼佼者,但是它的标准化只达到了(94%以上,但是之前一直优于MS C++,即便如此,大部分的Linux C++开发人员仍然亲睐GCC,因为它是OpenSource的)。
经过简单的一段介绍,我们应该对编译器有了一点感觉了,可是这和我们这篇主题有啥关系?我只是一个希望点一下“编译”按钮的读者,我并不希望makefile然后才编译。这一切似乎和我没啥关系。不过我告诉你,就了Eclipse+CDT,你仍然做不到这点。因为就算是针对C/C++的开发,我们仍然要经常面临使用不同的编译器进行编译的情形。甚至有些语法特性和标准C++并无任何关联,而仅与不同的编译器有关。又或者有些源代码在不同的编译器编译下会产生微妙不同的目标代码,而这一些则需要我们更好地理解。就算我们期望有一种通用或者说是常用的方式来解决这个问题,我们仍然需要手动为自己配置一个简单而通用的编译器,尽管你从不了解编译器方面的差异,你起码也得有一个编译器。那么好吧,既然Eclipse的安装如此不尽人意,那我就只能自己动手了。
如果您看过我的上两篇文章,您或许已经跟着做了,如果没有,那么重复一遍。(《如何安装MinGW 》、《如何安装Minimal SYStem(MSYS)》)
如果您照上面那么做了,那么您的机器上已经有了MinGW和MSYS了,换言之,您已经有了gcc、make以及其它一些必要文件了。下面您需要做的只要设定一下环境变量即可。
设置环境变量步骤:
右键“我的电脑”(没有“我的电脑”的用户在桌面右键“属性”,然后再“桌面”选项卡中自定义中将其选出,或者直接在Windows Explorer地址栏中输入“我的电脑”,回车即可),选择“属性”,选择“高级”选项卡,最下方有“环境变量”(或Alt+N打开),在下面窗格“系统变量”中,选择变量“Path”,双击后出现“编辑系统变量”的选项。将我们MinGW的bin目录全局路径复制进去(一般可以复制在“变量值”的最前面,或者紧跟在任何一个“;”(分号,不包括引号)后),点击确定,即可。
如果我现在告诉您可以编辑C/C++代码了,您相信么?
好吧,那么就开始我们的第一个C/C++代码的测试,以验证我们的成果。
一个HelloWorld的基本步骤:
1、打开Eclipse
2、选择一个“工作空间”(workspace)
3、选择一个“工程”(Project)
4、在向导中设置项目名称。这里我们将项目名称设定为HelloEclipse,在存放路径上,我们需要有所讲究,如果我们勾选Use default location的话,我们的location位置将是步骤1中设置的路径,我们通常需要设立子文件夹,以避免工程数量的增加而导致不同工程文件的交错。深刻理解这一点,把两种方案都试一下就可以了。
在Project types(项目类型)中,我们可以选择一个Hello World ANSI C Project,在Toolchain中选择MinGW GCC,这就是我们本机所拥有的编译器了。
5、因为我们选择了Hello World项目,所以我们还有一个基本设置的页面,可以输入一些个性化的信息。
6、如果您是在Windows上使用,则可以选择高级设置(Advanced settings),选择二进制转换器的类型(如图),当然,默认情况下是选好的因此,不用顾及该部分内容也依然会成功,但如果您遇到诸如此类的问题,则可以看看这里是否正确设置了。
相关路径:C/C++ Build -> Settings -> Binary Parsers
点击“OK”或者“Finish”即可完成。
7、编写个代码试试(其实系统已经为我们在src文件夹下生成了一个)
让我们写一个简单的代码试试
代码如下:
#include <stdio.h>
void myputs(char * s);
int main()
{
char * s = "HelloEclipse!";
myputs(s);
return 0;
}
void myputs(char * s)
{
while(*s)
{
printf("%c",*s++);
}
return;
}
另外在菜单Project中有很多Build相关的选项,选择Build Project后,等待编译结束,然后点击“Run”(绿色图标),在最下方的选项卡中找到Console,即可看到输出结果。
用惯了GCC编译器,也就容易将C语言的写法和其它的写法混淆起来。虽然在GCC平台上可以顺利编译,但是在其它编译器比如Microsoft C++编译器下就有可能编译出错了。看下面这段代码:
#include <stdio.h>
int main(void){
char *s1 ;
s1 = "Hello";
char *s2 ;
s2 = "World";
printf("%s %s\n",s1,s2);
return 0;
}
它看上去是可以执行的,在GCC编译器下它确实也是可以执行的。但在VC++中则不能执行,准确地说,在未开启编译器选项为标准C99的情况下,是会编译出错的。
按如上所示的编译器在C89标准下编译,GCC pass,MS C++ fatal。错误指示会在s2 = "World"; 这句话上,错误代码通常为
error C2143:语法错误:缺少“;”(在“类型”的前面)
要是您的类型刚好由typedef来定义的话,则会出现错误代码:
error C2275:“your_type”:将此类型用作表达式非法
下面两幅截图展示了以上两种错误。因为在变量声明环节出了错误,因此会引发一连串的错误,包括变量未定义等错误。
建议:为了保持源码能够保持跨编译器特性,我们最好能够按旧时的写法来写(C89)直到它确实被淘汰为止。