一.思想过渡
前言:明确地说,学了C语言就相当于学了 C++ 的一半,从C语言转向 C++ 时,不需要再从头开始,接着C语言往下学就可以,所以我强烈建议先学C语言再学 C++。
1.面向过程与面向对象
从“学院派”的角度来说,C++ 支持面向过程编程、面向对象编程和泛型编程,而C语言仅支持面向过程编程。就面向过程编程而言,C++ 和C几乎是一样的,所以学习了C语言,也就学习了 C++ 的一半,不需要从头再来。
面向过程(POP):
- 以过程(Procedure)为中心的编程方式
- 按照计算机执行的步骤,按从上到下顺序设计程序
面向对象(OOP):
- 以对象 (Object)为核心的编程范式
- 对象是类(Class)的实例,类中包括了数据的定义和对数据的操作方法
2.类和对象
C++是一门面向对象的编程语言,理解 C++,首先要理解**类(Class)和对象(Object)**这两个概念。
C++ 中的类(Class)可以看做C语言中结构体(Struct
)的升级版。结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。
C++ 中的类也是一种构造类型,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数;通过类定义出来的变量也有特定的称呼,叫做“对象”。
#include <stdio.h>
//通过class关键字类定义类
class Student{
public:
//类包含的变量
char *name;
int age;
float score;
//类包含的函数
void say(){
printf("%s的年龄是 %d,成绩是 %f\n", name, age, score);
}
};
int main(){
//通过类来定义变量,即创建对象
class Student stu1; //也可以省略关键字class
//为类的成员变量赋值
stu1.name = "小明";
stu1.age = 20;
stu1.score = 92.5f;
//调用类的成员函数
stu1.say();
return 0;
}
C语言中的 struct
只能包含变量,而 C++ 中的 class 除了可以包含变量,还可以包含函数。display() 是用来处理成员变量的函数,在C语言中,我们将它放在了struct Student
外面,它和成员变量是分离的;而在 C++ 中,我们将它放在了 class Student 内部,使它和成员变量聚集在一起,看起来更像一个整体。
注意:对于熟悉 C++ 的读者,这段代码并不规范,请忽略这一细节,主要是从C到C++过渡。
结构体和类都可以看做一种由用户自己定义的复杂数据类型,在C语言中可以通过结构体名来定义变量,在 C++ 中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)。
在 C++ 中,通过类名就可以创建对象,即将图纸生产成零件,这个过程叫做类的实例化,因此也称对象是类的一个实例(Instance)。有些资料也将类的成员变量称为属性(Property),将类的成员函数称为方法(Method)。
二.命名空间
1.原因
一个中大型软件往往由多名程序员共同开发,会使用大量的变量和函数,不可避免地会出现变量或函数的命名冲突。当所有人的代码都测试通过,没有问题时,将它们结合到一起就有可能会出现命名冲突。
例如小李和小韩都参与了一个文件管理系统的开发,它们都定义了一个全局变量 fp
,用来指明当前打开的文件,将他们的代码整合在一起编译时,很明显编译器会提示fp
重复定义(Redefinition)错误。
为了解决合作开发时的命名冲突问题,C++引入了命名空间的概念:
namespace Li{ //小李的变量定义
FILE* fp = NULL;
}
namespace Han{ //小韩的变量定义
FILE* fp = NULL;
}
2.语法格式
namespace
是C++中的关键字,用来定义一个命名空间,语法格式为:
namespace name{
//variables, functions, classes //在这里面可以定义自己使用的任意变量、类、对象、函数
}
name
是命名空间的名字,它里面可以包含变量、函数、类、typedef、#define 等,最后由{ }
包围。
使用变量、函数时要指明它们所在的命名空间。以上面的fp
变量为例,可以这样来使用:
Li::fp = fopen("one.txt", "r"); //使用小李定义的变量 fp
Han::fp = fopen("two.txt", "rb+"); //使用小韩定义的变量 fp
::
是一个新符号,称为域解析操作符,在C++中用来指明要使用的命名空间。
除了直接使用域解析操作符,还可以采用 using
关键字声明:
using Li::fp;
fp = fopen("one.txt", "r"); //使用小李定义的变量 fp
Han :: fp = fopen("two.txt", "rb+"); //使用小韩定义的变量 fp
using
声明以后的程序中如果出现了未指明命名空间的 fp
,就使用 Li::fp
;但是若要使用小韩定义的 fp
,仍然需要 Han::fp
。
using
声明不仅可以针对命名空间中的一个变量,也可以用于声明整个命名空间:
using namespace Li;
fp = fopen("one.txt", "r"); //使用小李定义的变量 fp
Han::fp = fopen("two.txt", "rb+"); //使用小韩定义的变量 fp
在 using
声明后,如果有未具体指定命名空间的变量产生了命名冲突,那么默认采用命名空间 Li 中的变量。
3.标准命名空间
C++是在C语言的基础上开发的,早期的 C++ 还不完善,不支持命名空间,没有自己的编译器,而是将 C++ 代码翻译成C代码,再通过C编译器完成编译。这个时候的 C++ 仍然在使用C语言的库,stdio.h
、stdlib.h
、string.h
等头文件依然有效;此外 C++ 也开发了一些新的库,增加了自己的头文件。
和C语言一样,C++ 头文件仍然以.h
为后缀,它们所包含的类、函数、宏等都是全局范围的。后来 C++ 引入了命名空间的概念,计划重新编写库,将类、函数、宏等都统一纳入一个命名空间,这个命名空间的名字就是std
。
理论上可以发现,对于不带.h
的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;对于带.h
的头文件,没有使用任何命名空间,所有符号都位于全局作用域。这也是 C++ 标准所规定的。
下面是一个完整使用C++头文件和命名空间的例子:
#include <iostream>
#include <string>
/*如果在这里使用using namespace std; 那么代表这整个.cpp的文件都是std下的,这对于我们平时练习一个的.cpp文件没有区别
但如果是在多个.cpp文件构成的项目中或者一个.cpp文件类需要用到多个命名空间时,还是写在函数内部比较好*/
int main() {
//声明命名空间std
using namespace std;
//定义字符串变量
string str;
//定义 int 变量
int age;
//从控制台获取用户输入
cin >> str >> age;
//将数据输出到控制台
cout << str << "已经" << age << "岁了!" << endl;
return 0;
}
将 std 直接声明在所有函数外部,这样虽然使用方便,但在中大型项目开发中是不被推荐的,这样做增加了命名冲突的风险,我推荐在函数内部声明 std。
三.操作转变
1.输入与输出
在C语言中,我们通常会使用 scanf
和printf
来对数据进行输入输出操作,在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件iostream
,它包含了用于输入输出的对象,例如常见的cin
表示标准输入、cout
表示标准输出、cerr
表示标准错误。
cout 和 cin 都是 C++ 的内置对象,而不是关键字。C++ 库定义了大量的类(Class),程序员可以使用它们来创建对象,cout 和 cin 就分别是 ostream
和 istream
类的对象,只不过它们是由标准库的开发者提前创建好的,可以直接拿来使用。这种在 C++ 中提前创建好的对象称为内置对象。
使用 cout
进行输出时需要紧跟<<
运算符,使用 cin
进行输入时需要紧跟>>
运算符,这两个运算符可以自行分析所处理的数据类型,因此无需像使用 scanf
和 printf
那样给出格式控制字符串。
2.布尔类型
C语言并没有彻底从语法上支持“真”和“假”,只是用 0 和非 0 来代表。这点在 C++ 中得到了改善,C++ 新增了bool
类型(布尔类型),它一般占用 1 个字节长度。bool
类型只有两个取值,true
和 false
:true
表示“真”,false
表示“假”。
遗憾的是,在 C++ 中使用 cout
输出 bool
变量的值时还是用数字 1 和 0 表示,而不是 true 或 false。但你也可以使用 true 或 false 显式地对 bool
变量赋值。
注意:虽然我们平时写C语言代码时,习惯了
bool
类型,但是在标准的只有C语言的IDE中如果运行包含bool
类型的代码是会报错的,C语言并不支持bool
类型,只不过我们平时用的都是.cpp
文件格式写的是C语言,所以正确来说是C语言和C++的结合版本,那么C语言中的true
和false
怎么表示呢?用int
类型即可,>0表示真,=0表示假。
3.动态分配空间
在C语言中,动态分配内存用 malloc()
函数,释放内存用 free() 函数。如下所示:
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间
free(p); //释放内存
在C++中,这两个函数仍然可以使用,但是C++又新增了两个关键字,new
和 delete
:new
用来动态分配内存,delete
用来释放内存。
用 new 和 delete 分配内存更加简单:
int *p = new int; //分配1个int型的内存空间
delete p; //释放内存
如果希望分配一组连续的数据,可以使用 new[]:
int *p = new int[10]; //分配10个int型的内存空间
delete[] p;
和 malloc()
一样,new
也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。为了避免内存泄露,通常new
和 delete
、new[]
和delete[]
操作符应该成对出现,并且不要和C语言中 malloc()
、free()
一起混用。
四.const
用法
1.修饰全局变量
关键字const
用来定义常量,如果一个变量被const
修饰,那么它的值就不能再被改变,我们通常称这样的变量为常量。由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误。
我们知道,在C语言中普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern
声明后就可以使用:
代码段1(源文件1):
#include <stdio.h>
int n = 10;
void func();
int main(){
func();
printf("main: %d\n", n);
return 0;
}
代码段2(源文件2):
#include <stdio.h>
extern int n;
void func(){
printf("module: %d\n", n);
}
不管是以C还是C++的方式编译,运行结果都是:
module: 10 main: 10
在C语言中,const
变量和普通变量一样,在其他源文件中也是可见的。修改代码段1,在 n 的定义前面加const
限制,如下所示:
const int n = 10;
修改后的代码仍然能够正确编译,运行结果和上面也是一样的。这说明C语言中的 const
变量在多文件编程时的表现和普通变量一样,除了不能修改,没有其他区别。
但是如果按照C++的方式编译(将源文件后缀设置为.cpp
),修改后的代码就是错误的。这是因为 C++ 对 const
的特性做了调整,C++ 规定,全局 const
变量的作用域仍然是当前文件,但是它在其他文件中是不可见的,这和添加了static
关键字的效果类似。虽然代码段2中使用 extern 声明了变量 n,但是在链接时却找不到代码段1中的 n。
因为C++中全局
const
变量的可见范围是当前文件,所以如果在C++中要使用同一项目下其他.cpp
文件定义的全局变量(被const
修饰),那么可以把const
修饰的变量定义在头文件(.h文件中)通过引入头文件的方式来解决该类问题。
2.常量指针与指针常量
常量指针是指针指向的内容是常量,可以有一下两种定义方式:
const int * n;
int const * n;
这是因为type
和const
是可以互换位置的,需要注意以下两点:
- 1.常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。
- 2.常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。
指针常量是指指针本身是个常量,不能在指向其他的地址,写法如下:
int *const n;
需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。
小技巧:区分常量指针和指针常量的关键就在于星号的位置,我们以星号为分界线,如果
const
在星号的左边,则为常量指针,如果const
在星号的右边则为指针常量。如果我们将星号读作‘指针’,将const
读作‘常量’的话,内容正好符合。int const * n;
是常量指针,int *const n;
是指针常量。
3.修饰函数参数
const
通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据(该数据既可以是变量也可以是地址,取决于你使用的是常量指针还是指针常量),就可以用 const
来限制。
4.内存角度
先来看下面的两条语句:
const int m = 10;
int n = m;
我们知道,变量是要占用内存的,即使被 const
修饰也不例外。m、n 两个变量占用不同的内存,int n = m;
表示将 m 的值赋给 n,这个赋值的过程在C和C++中是有区别的。
在C语言中,编译器会先到 m 所在的内存取出一份数据,再将这份数据赋给 n;而在C++中,编译器会直接将 10 赋给 n,没有读取内存的过程,和int n = 10;
的效果一样。C++ 中的常量更类似于#define
命令,是一个值替换的过程,只不过#define
是在预处理阶段替换,而常量是在编译阶段替换。
C++ 对const
的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值。
五.函数角度
1.内联函数
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数,又称内嵌函数或者内置函数。
指定内联函数的方法很简单,只需要在函数定义处增加 inline
关键字。
注意,要在函数定义处添加
inline
关键字,在函数声明处添加inline
关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的inline
关键字。
内联函数的另一个最大用处是替换宏:在C语言中我们置断宏是可以带参数的,它在形式上和函数非常相似。不过不像函数,宏仅仅是字符串替换,不是按值传递,所以在编写宏时要特别注意,一不小心可能就会踩坑。如果我们将宏替换为内联函数,情况就没有那么复杂了,通过内联函数来解决宏的按值传递问题是一个常见的用法。
2.默认参数
C++定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。
//带默认参数的函数
void func(int n, float b=1.2, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
//为所有参数传值
func(10, 3.5, '#');
//为n、b传值,相当于调用func(20, 9.8, '@')
func(20, 9.8);
//只为n传值,相当于调用func(30, 1.2, '@')
func(30);
注意:
- 默认参数除了使用数值常量指定,也可以使用表达式指定
- C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。
3.函数重载
在实际开发中,有时候我们需要实现几个功能类似的函数,只是有些细节不同。例如希望交换两个变量的值,这两个变量有多种类型,可以是 int、float、char、bool
等,我们需要通过参数把变量的地址传入函数内部。在C语言中,程序员往往需要分别设计出三个不同名的函数,其函数原型与下面类似:
void swap1(int *a, int *b); //交换 int 变量的值
void swap2(float *a, float *b); //交换 float 变量的值
void swap3(char *a, char *b); //交换 char 变量的值
void swap4(bool *a, bool *b); //交换 bool 变量的值
但在C++中,这完全没有必要。C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。借助重载,一个函数名可以有多种用途。
参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。
函数的重载的规则:
- 函数名称必须相同。
- 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载。
4.函数重载优先级
C++ 标准规定,在进行重载决议时编译器应该按照下面的优先级顺序来处理实参的类型:
优先级 | 包含的内容 | 举例说明 |
精确匹配 | 不做类型转换,直接匹配 | (暂无说明) |
只是做微不足道的转换 | 从数组名到数组指针、从函数名到指向函数的指针、从非 | |
类型提升后匹配 | 整型提升 | 从 |
小数提升 | ||
使用自动类型转换后匹配 | 整型转换 | 从 char 到 long、short 到 long、int 到 short、long 到 char。 |
小数转换 | 从 double 到 float。 | |
整数和小数转换 | 从 int 到 double、short 到 float、float 到 int、double 到 long。 | |
指针转换 | 从 int * 到 void *。 |
C++ 标准还规定,编译器应该按照从高到低的顺序来搜索重载函数,首先是精确匹配,然后是类型提升,最后才是类型转换;一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。
如果在一个优先级中找到多个(两个以及以上)合适的重载函数,编译器就会陷入两难境地,不知道如何抉择,编译器会将这种模棱两可的函数调用视为一种错误,因为这些合适的重载函数同等“优秀”,没有一个脱颖而出,调用谁都一样。这就是函数重载过程中的二义性错误。
注意:类型提升和类型转换不是一码事!类型提升是积极的,是为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低;而类型转换是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。类型提升只有上表中列出的几种情况,其他情况都是类型转换。
六.extern
与extern "c"
1.关键字extern
利用关键字extern
,根据作用域不同,大概有两种作用:
- 1.引用同一文件中的变量
因为变量
num
在main函数后面,如果按照过程(顺序)执行的话,计算机不会知道num
的值,使用extern int num;
后可以告诉计算机存在这个值,往后面找.#include<stdio.h> int func(); int main() { func(); //1 extern int num; printf("%d",num); //2 return 0; } int num = 3; int func() { printf("%d\n",num); }
- 2.引用同一项目下另一文件的变量或函数:结合
const
修饰全局变量理解。
2.extern "c"
如果你在编写项目的过程中,使用的是C和C++混合编程,考虑到对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。为了避免函数以不同的编译方式处理,我们应该使其在main.cpp
文件中仍以 C 语言代码的方式处理,这样就可以解决函数名不一致的问题。
extern
是 C 和 C++ 的一个关键字,但对于 extern "C"
,我们大可以将其看做一个整体,和 extern
毫无关系。
extern "C"
既可以修饰一句 C++ 代码,也可以修饰一段 C++ 代码,它的功能是让编译器以处理 C 语言代码的方式来处理修饰的 C++ 代码。