C++命名空间(名字空间)详解

命名冲突问题,C++ 引入了命名空间(Namespace)的概念

1
2
3
4
5
6
namespace Li{ 
FILE* fp = NULL;
}
namespace Han{
FILE* fp = NULL;
}

namespace 是C++中的关键字,用来定义一个命名空间
语法格式为:
namespace name{ //variables, functions, classes}

name是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define 等,最后由{ }包围。

使用变量、函数时要指明它们所在的命名空间。以上面的 fp 变量为例,可以这样来使用:

1
2
Li::fp = fopen("one.txt", "r"); 
Han::fp = fopen("two.txt", "rb+");

:: 是一个新符号,称为域解析操作符,在C++中用来指明要使用的命名空间。

除了直接使用域解析操作符,还可以采用 using 关键字声明:

1
2
3
using Li::fp;
fp = fopen("one.txt", "r");
Han :: fp = fopen("two.txt", "rb+");

在代码的开头用using声明了 Li::fp,它的意思是,using 声明以后的程序中如果出现了未指明命名空间的 fp,就使用 Li::fp;但是若要使用小韩定义的 fp,仍然需要 Han::fp。

using 声明不仅可以针对命名空间中的一个变量,也可以用于声明整个命名空间:

1
2
3
using namespace Li;
fp = fopen("one.txt", "r");
Han::fp = fopen("two.txt", "rb+");

如果命名空间 Li 中还定义了其他的变量,那么同样具有 fp 变量的效果。在 using 声明后,如果有未具体指定命名空间的变量产生了命名冲突,那么默认采用命名空间 Li 中的变量。

命名空间内部不仅可以声明或定义变量,对于其它能在命名空间以外声明或定义的名称,同样也都能在命名空间内部进行声明或定义,例如类、函数、typedef、#define 等都可以出现在命名空间中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
//将类定义在命名空间中
namespace Diy{
class Student{
public:
char *name;
int age;
float score;
public:
void say(){
printf("%s的年龄是 %d,成绩是 %f\n", name, age, score);
}
};
}

int main(){
Diy::Student stu1;
stu1.name = "小明";
stu1.age = 15;
stu1.score = 92.5f;
stu1.say();

return 0;
}

C++头文件和std命名空间

和C语言一样,C++ 头文件仍然以.h为后缀,它们所包含的类、函数、宏等都是全局范围的。

后来 C++ 引入了命名空间的概念,计划重新编写库,将类、函数、宏等都统一纳入一个命名空间,这个命名空间的名字就是std。

C++ 开发人员想了一个好办法,保留原来的库和头文件,它们在 C++ 中可以继续使用,然后再把原来的库复制一份,在此基础上稍加修改,把类、函数、宏等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。

为了避免头文件重名,新版 C++ 库也对头文件的命名做了调整,去掉了后缀.h,所以老式 C++ 的iostream.h变成了iostream,fstream.h变成了fstream。
而对于原来C语言的头文件,也采用同样的方法,但在每个名字前还要添加一个c字母,所以C语言的stdio.h变成了cstdio,stdlib.h变成了cstdlib。

总结的 C++ 头文件的现状:

  1. 旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中

  2. 新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间 std 中。

注意:在标准化的过程中,库中有些部分的细节被修改了,所以旧的头文件和新的头文件不一定完全对应。

  1. 标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中。

  2. 具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,只是内容在 std 中。

使用C++的头文件

在 main() 函数中声明命名空间 std,它的作用范围就位于 main() 函数内部,如果在其他函数中又用到了 std,就需要重新声明。

很多教程中都是这样做的,将 std 直接声明在所有函数外部,这样虽然使用方便,但在中大型项目开发中是不被推荐的,这样做增加了命名冲突的风险,我推荐在函数内部声明 std。

C++输入输出(cin和cout)

在C++语言中,C语言的这一套输入输出库我们仍然能使用,但是 C++ 又增加了一套新的、更容易使用的输入输出库。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int main(){
int x;
float y;
cout<<"Please input an int number:"<<endl;
cin>>x;
cout<<"The int number is x= "<<x<<endl;
cout<<"Please input a float number:"<<endl;
cin>>y;
cout<<"The float number is y= "<<y<<endl;
return 0;
}

C++ 中的输入与输出可以看做是一连串的数据流,输入即可视为从文件或键盘中输入程序中的一串数据流,而输出则可以视为从程序中输出一连串的数据流到显示屏或文件中。

在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件iostream,它包含了用于输入输出的对象

cout 和 cin 都是 C++ 的内置对象,而不是关键字。C++ 库定义了大量的类(Class),程序员可以使用它们来创建对象,cout 和 cin 就分别是 ostream 和 istream 类的对象,只不过它们是由标准库的开发者提前创建好的,可以直接拿来使用。这种在 C++ 中提前创建好的对象称为内置对象。

使用 cout 进行输出时需要 << 运算符

使用 cin 进行输入时需要 >> 运算符

这两个运算符可以自行分析所处理的数据类型,因此无需像使用 scanf 和 printf 那样给出格式控制字符串。

第 6 行代码表示输出”Please input a int number:”这样的一个字符串,以提示用户输入整数,

endl表示换行,与C语言里的\n作用相同。
这段代码中也可以用\n来替代endl,这样就得写作:
cout<<”Please input an int number:\n”;

第 7 行代码表示从标准输入(键盘)中读入一个 int 型的数据并存入到变量 x 中。如果此时用户输入的不是 int 型数据,则会被强制转化为 int 型数据。

第 8 行代码将输入的整型数据输出。从该语句中我们可以看出 cout 能够连续地输出。

cin 也是支持对多个变量连续输入的

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
int main(){
int x;
float y;
cout<<"Please input an int number and a float number:"<<endl;
cin>>x>>y;
cout<<"The int number is x= "<<x<<endl;
cout<<"The float number is y= "<<y<<endl;
return 0;
}

第 7 行代码连续从标准输入中读取一个整型和一个浮点型数字(默认以空格分隔),分别存入到 x 和 y 中。输入运算符>>在读入下一个输入项前会忽略前一项后面的空格,所以数字 8 和 7.4 之间要有一个空格,当 cin 读入 8 后忽略空格,接着读取 7.4。

C++布尔类型(bool)

在C语言中,关系运算和逻辑运算的结果有两种,真和假:0 表示假,非 0 表示真。例如:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int a, b, flag;
scanf("%d %d", &a, &b);
flag = a > b;
printf("flag = %d\n", flag);

return 0;
}

C语言并没有彻底从语法上支持“真”和“假”,只是用 0 和非 0 来代表。这点在 C++ 中得到了改善
C++ 新增了 bool 类型(布尔类型),它一般占用 1 个字节长度。

bool 类型只有两个取值,true 和 false:true 表示“真”,false 表示“假”。

bool 是类型名字,也是 C++ 中的关键字,它的用法和 int、char、long 是一样的

在 C++ 中使用 cout 输出 bool 变量的值时还是用数字 1 和 0 表示,而不是 true 或 false。

C++ new和delete运算符简介

在C语言中,动态分配内存用 malloc() 函数,释放内存用 free() 函数。如下所示:

int p = (int) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间
free(p); //释放内存

int *p = new int; //分配1个int型的内存空间
delete p; //释放内存

new 操作符会根据后面的数据类型来推断所需空间的大小。

如果希望分配一组连续的数据,可以使用 new[]:

int *p = new int[10]; //分配10个int型的内存空间
delete[] p;

用 new[] 分配的内存需要用 delete[] 释放,它们是一一对应的。

C++ string详解,C++字符串详解

C++ 大大增强了对字符串的支持,除了可以使用C风格的字符串,还可以使用内置的 string 类。string 类处理起字符串来会方便很多,完全可以代替C语言中的字符数组或字符串指针。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1;
string s2 = "c plus plus";
string s3 = s2;
string s4 (5, 's');
return 0;
}

变量 s1 只是定义但没有初始化,编译器会将默认值赋给 s1,默认值是””,也即空字符串。

变量 s2 在定义的同时被初始化为”c plus plus”。与C风格的字符串不同,string 的结尾没有结束标志’\0’。

变量 s3 在定义的时候直接用 s2 进行初始化,因此 s3 的内容也是”c plus plus”。

变量 s4 被初始化为由 5 个’s’字符组成的字符串,也就是”sssss”。

从上面的代码可以看出,string 变量可以直接通过赋值操作符=进行赋值
string 变量也可以用C风格的字符串进行赋值,例如,s2 是用一个字符串常量进行初始化的,而 s3 则是通过 s2 变量进行初始化的。

与C风格的字符串不同,当我们需要知道字符串长度时,可以调用 string 类提供的 length() 函数。如下所示:

1
2
3
string s = "http://c.biancheng.net";
int len = s.length();
cout<<len<<endl;

string 的末尾没有’\0’字符,所以 length() 返回的是字符串的真实长度,而不是长度 +1

转换为C风格的字符串

虽然 C++ 提供了 string 类来替代C语言中的字符串,但是在实际编程中,有时候必须要使用C风格的字符串(例如打开文件时的路径)
string 类为我们提供了一个转换函数 c_str(),该函数能够将 string 字符串转换为C风格的字符串,并返回该字符串的 const 指针(const char*)

1
2
string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");

为了使用C语言中的 fopen() 函数打开文件,必须将 string 字符串转换为C风格的字符串。

string 字符串的输入输出

string 类重载了输入输出运算符,可以像对待普通变量那样对待 string 变量,也就是用>>进行输入,用<<进行输出

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>

using namespace std;

int main(){
string s;
cin>>s; //输入字符串
cout<<s<<endl; //输出字符串
return 0;
}
访问字符串中的字符

string 字符串也可以像C风格的字符串一样按照下标来访问其中的每一个字符。string 字符串的起始下标仍是从 0 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#include <iostream>
#include <string>
using namespace std;

int main(){
string s = "1234567890";
for(int i=0,len=s.length(); i<len; i++){
cout<<s[i]<<" ";
}
cout<<endl;
s[5] = '5';
cout<<s<<endl;
return 0;
}
字符串的拼接

有了 string 类,可以使用 + 或 += 运算符来直接拼接字符串,非常方便,也不需要使用C语言中的 strcat()、strcpy()、malloc() 等函数来拼接字符串了,不用担心空间不够会溢出了。

用+来拼接字符串时,运算符的两边可以都是 string 字符串,也可以是一个 string 字符串和一个C风格的字符串,还可以是一个 string 字符串和一个字符数组,或者是一个 string 字符串和一个单独的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1 = "first ";
string s2 = "second ";
char *s3 = "third ";
char s4[] = "fourth ";
char ch = '@';

string s5 = s1 + s2;
string s6 = s1 + s3;
string s7 = s1 + s4;
string s8 = s1 + ch;

cout<<s5<<endl<<s6<<endl<<s7<<endl<<s8<<endl;

return 0;
}
string 字符串的增删改查

插入字符串

insert() 函数可以在 string 字符串中指定的位置插入另一个字符串
它的一种原型为:
string& insert (size_t pos, const string& str);

pos 表示要插入的位置,也就是下标;
str 表示要插入的字符串,它可以是 string 字符串,也可以是C风格的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1, s2, s3;
s1 = s2 = "1234567890";
s3 = "aaa";
s1.insert(5, s3);
cout<< s1 <<endl;
s2.insert(5, "bbb");
cout<< s2 <<endl;
return 0;
}

删除字符串

erase() 函数可以删除 string 中的一个子字符串。
它的一种原型为:
string& erase (size_t pos = 0, size_t len = npos);

pos 表示要删除的子字符串的起始下标,len 表示要删除子字符串的长度。
如果不指明 len 的话,那么直接删除从 pos 到字符串结束处的所有字符(此时 len = str.length - pos)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1, s2, s3;
s1 = s2 = s3 = "1234567890";
s2.erase(5);
s3.erase(5, 3);
cout<< s1 <<endl;
cout<< s2 <<endl;
cout<< s3 <<endl;
return 0;
}

提取子字符串

substr() 函数
用于从 string 字符串中提取子字符串
它的原型为:
string substr (size_t pos = 0, size_t len = npos) const;

pos 为要提取的子字符串的起始下标,len 为要提取的子字符串的长度。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1 = "first second third";
string s2;
s2 = s1.substr(6, 6);
cout<< s1 <<endl;
cout<< s2 <<endl;
return 0;
}

系统对 substr() 参数的处理和 erase() 类似:如果 pos 越界,会抛出异常;如果 len 越界,会提取从 pos 到字符串结尾处的所有字符。

字符串查找

string 类提供了几个与字符串查找有关的函数

  1. find() 函数

find() 函数用于在 string 字符串中查找子字符串出现的位置
它其中的两种原型为:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;

第一个参数为待查找的子字符串,它可以是 string 字符串,也可以是C风格的字符串。第二个参数为开始查找的位置(下标);如果不指明,则从第0个字符开始查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1 = "first second third";
string s2 = "second";
int index = s1.find(s2,5);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}

find() 函数最终返回的是子字符串第一次出现在字符串中的起始下标。
本例最终是在下标6处找到了 s2 字符串。如果没有查找到子字符串,那么会返回一个无穷大值 4294967295。

  1. rfind() 函数

rfind() 和 find() 很类似,同样是在字符串中查找子字符串,不同的是 find() 函数从第二个参数开始往后查找,而 rfind() 函数则最多查找到第二个参数处,如果到了第二个参数所指定的下标还没有找到子字符串,则返回一个无穷大值4294967295。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1 = "first second third";
string s2 = "second";
int index = s1.rfind(s2,6);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}
  1. find_first_of() 函数

find_first_of() 函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
using namespace std;

int main(){
string s1 = "first second second third";
string s2 = "asecond";
int index = s1.find_first_of(s2);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl;
else
cout<<"Not found"<<endl;
return 0;
}

C++引用

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。对于像 char、bool、int、float 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能会消耗很多时间,拖慢程序的执行效率。

在 C++ 中,我们有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用

引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。

引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序

引用的定义方式类似于指针,只是用&取代了*,语法格式为:type &name = data;

type 是被引用的数据的类型,name 是引用的名称,data 是被引用的数据。
引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main() {
int a = 99;
int &r = a;
cout << a << ", " << r << endl;
cout << &a << ", " << &r << endl;

return 0;
}

也可以说变量 r 是变量 a 的另一个名字。从输出结果可以看出,a 和 r 的地址一样,都是0x28ff44;或者说地址为0x28ff44的内存有两个名字,a 和 r,想要访问该内存上的数据时,使用哪个名字都行。

注意,引用在定义时需要添加&,在使用时不能添加&,使用时添加&表示取地址。了这两种用法,&还可以表示位运算中的与运算。

由于引用 r 和原始变量 a 都是指向同一地址,所以通过引用也可以修改原始变量中所存储的数据

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;

int main() {
int a = 99;
int &r = a;
r = 47;
cout << a << ", " << r << endl;

return 0;
}

引用作为函数参数

在定义或声明函数时,我们可以将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有“在函数内部影响函数外部数据”的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
using namespace std;

void swap1(int a, int b);
void swap2(int *p1, int *p2);
void swap3(int &r1, int &r2);


int main() {
int num1, num2;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap1(num1, num2);
cout << num1 << " " << num2 << endl;

cout << "Input two integers: ";
cin >> num1 >> num2;
swap2(&num1, &num2);
cout << num1 << " " << num2 << endl;

cout << "Input two integers: ";
cin >> num1 >> num2;
swap3(num1, num2);
cout << num1 << " " << num2 << endl;

return 0;
}

//直接传递参数内容
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}

//传递指针
void swap2(int *p1, int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}

//按引用传参
void swap3(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;
}

三种交换变量的值的方法:

  1. swap1() 直接传递参数的内容,不能达到交换两个数的值的目的。对于 swap1() 来说,a、b 是形参,是作用范围仅限于函数内部的局部变量,它们有自己独立的内存,和 num1、num2 指代的数据不一样。调用函数时分别将 num1、num2 的值传递给 a、b,此后 num1、num2 和 a、b 再无任何关系,在 swap1() 内部修改 a、b 的值不会影响函数外部的 num1、num2,更不会改变 num1、num2 的值。

  2. swap2() 传递的是指针,能够达到交换两个数的值的目的。调用函数时,分别将 num1、num2 的指针传递给 p1、p2,此后 p1、p2 指向 a、b 所代表的数据,在函数内部可以通过指针间接地修改 a、b 的值。我们在《C语言指针变量作为函数参数》中也对比过第 1)、2) 中方式的区别。

  3. swap3() 是按引用传递,能够达到交换两个数的值的目的。调用函数时,分别将 r1、r2 绑定到 num1、num2 所指代的数据,此后 r1 和 num1、r2 和 num2 就都代表同一份数据了,通过 r1 修改数据后会影响 num1,通过 r2 修改数据后也会影响 num2。

从以上代码的编写中可以发现,按引用传参在使用形式上比指针更加直观。在以后的 C++ 编程中,我鼓励读者大量使用引用,它一般可以代替指针(当然指针在C++中也不可或缺),C++ 标准库也是这样做的。

C++引用作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

int &plus10(int &r) {
r += 10;
return r;
}

int main() {
int num1 = 10;
int num2 = plus10(num1);
cout << num1 << " " << num2 << endl;

return 0;
}

在将引用作为函数返回值时应该注意一个小问题,就是不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++ 编译器检测到该行为时也会给出警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int &plus10(int &r) {
int m = r + 10;
return m; //返回局部数据的引用
}

int main() {
int num1 = 10;
int num2 = plus10(num1);
cout << num2 << endl;
int &num3 = plus10(num1);
int &num4 = plus10(num3);
cout << num3 << " " << num4 << endl;

return 0;
}

plus10() 返回一个对局部变量 m 的引用,这是导致运行结果非常怪异的根源,因为函数是在栈上运行的,并且运行结束后会放弃对所有局部数据的管理权,后面的函数调用会覆盖前面函数的局部数据。本例中,第二次调用 plus10() 会覆盖第一次调用 plus10() 所产生的局部数据,第三次调用 plus10() 会覆盖第二次调用 plus10() 所产生的局部数据。