1.C++的类和对象
C++ 中的类(Class)可以看做C语言中结构体(Struct)的升级版。结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。
C++ 中的类也是一种构造类型,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数;通过类定义出来的变量也有特定的称呼,叫做“对象”。
2.编译
在C语言中,我们使用gcc
命令来编译和链接C程序。
gcc main.c
不过 GCC 中还有一个g++
命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。
g++ main.cpp
使用-o
选项可以指定可执行文件的名称:
g++ main.cpp -o demo
./demo
GCC 是由 GUN 组织开发的,最初只支持C语言,是一个单纯的C语言编译器,后来 GNU 组织倾注了更多的精力,使得 GCC 越发强大,增加了对 C++、Objective-C、Fortran、Java 等其他语言的支持,此时的 GCC 就成了一个编译器套件(套装),是所有编译器的总称。
GCC 又针对不同的语言推出了不同的命令,例如g++
命令用来编译 C++,gcj
命令用来编译 Java,gccgo
命令用来编译Go语言。
在以后使用 Linux GCC 时,我推荐使用g++
命令来编译 C++ 程序,这样更加简洁和规范。
3.C++命名空间(名字空间)
一个中大型软件往往由多名程序员共同开发,会使用大量的变量和函数,不可避免地会出现变量或函数的命名冲突。当所有人的代码都测试通过,没有问题时,将它们结合到一起就有可能会出现命名冲突。为了解决合作开发时的命名冲突问题,C++ 引入了命名空间(Namespace)的概念。
namespace Li{ //小李的变量定义
FILE* fp = NULL;
}
namespace Han{ //小韩的变量定义
FILE* fp = NULL;
}
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
声明了 Li::fp,它的意思是,using 声明以后的程序中如果出现了未指明命名空间的 fp,就使用 Li::fp;但是若要使用小韩定义的 fp,仍然需要 Han::fp。
using 声明不仅可以针对命名空间中的一个变量,也可以用于声明整个命名空间。
命名空间完整示例:
#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;
}
4.C++头文件和std命名空间
保留原来的库和头文件,它们在 C++ 中可以继续使用,然后再把原来的库复制一份,在此基础上稍加修改,把类、函数、宏等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。
为了避免头文件重名,新版 C++ 库也对头文件的命名做了调整,去掉了后缀.h
,所以老式 C++ 的iostream.h
变成了iostream
,fstream.h
变成了fstream
。而对于原来C语言的头文件,也采用同样的方法,但在每个名字前还要添加一个c
字母,所以C语言的stdio.h
变成了cstdio
,stdlib.h
变成了cstdlib
。
对于不带.h
的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;对于带.h
的头文件,没有使用任何命名空间,所有符号都位于全局作用域。这也是 C++ 标准所规定的。
在 main() 函数中声明命名空间 std,它的作用范围就位于 main() 函数内部,如果在其他函数中又用到了 std,就需要重新声明。如果希望在所有函数中都使用命名空间 std,可以将它声明在全局范围中。
5.C++输入和输出
C++ 中的输入与输出可以看做是一连串的数据流,输入即可视为从文件或键盘中输入程序中的一串数据流,而输出则可以视为从程序中输出一连串的数据流到显示屏或文件中。
在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件iostream
,它包含了用于输入输出的对象,例如常见的cin
表示标准输入、cout
表示标准输出、cerr
表示标准错误。
6.C++布尔类型
C++ 新增了 bool 类型(布尔类型),它一般占用 1 个字节长度。bool 类型只有两个取值,true 和 false:true 表示“真”,false 表示“假”。
7.C++ const
c语言中的const:
有时候我们希望定义这样一种变量,它的值不能被改变,在整个作用域中都保持固定。例如,用一个变量来表示班级的最大人数,或者表示缓冲区的大小。为了满足这一要求,可以使用const
关键字对变量加以限定:
const 离变量名近就是用来修饰指针变量的,离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。
const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。
C++中的const更像编译阶段的#define。
变量是要占用内存的,即使被 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++ 就不能取得最新的值。
如何通过指针修改 const 变量:
#include <stdio.h>
int main(){
const int n = 10;
int *p = (int*)&n; //必须强制类型转换
*p = 99; //修改const变量的值
printf("%d\n", n);
return 0;
}
注意,&n得到的指针的类型是const int *,必须强制转换为int *后才能赋给 p,否则类型是不兼容的。
将代码放到.c
文件中,以C语言的方式编译,运行结果为99
。再将代码放到.cpp
文件中,以C++的方式编译,运行结果就变成了10
。这种差异正是由于C和C++对 const 的处理方式不同造成的。
在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。而在C++中,printf("%d\n", n);
语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);
一样,不管 n 所在的内存如何变化,都不会影响输出结果。
C语言对 const 的处理和普通变量一样,会到内存中读取数据;C++ 对 const 的处理更像是编译时期的#define
,是一个值替换的过程。
C++中全局 const 变量的可见范围是当前文件。
这是因为 C++ 对 const 的特性做了调整,C++ 规定,全局 const 变量的作用域仍然是当前文件,但是它在其他文件中是不可见的,这和添加了static
关键字的效果类似。
C++ 中的 const 变量虽然也会占用内存,也能使用&
获取得它的地址,但是在使用时却更像编译时期的#define
;#define
也是值替换,可见范围也仅限于当前文件。
8.C++new和delete运算符
在C语言中,动态分配内存用 malloc() 函数,释放内存用 free() 函数。
int *p = (int*) malloc( sizeof(int) * 10 ); //分配10个int型的内存空间
free(p); //释放内存
在C++中,这两个函数仍然可以使用,但是C++又新增了两个关键字,new 和 delete:new 用来动态分配内存,delete 用来释放内存。new 操作符会根据后面的数据类型来推断所需空间的大小。
int *p = new int; //分配1个int型的内存空间
delete p; //释放内存
//分配一组连续数据
int *p = new int[10]; //分配10个int型的内存空间
delete[] p;
和 malloc() 一样,new 也是在堆区分配内存,必须手动释放,否则只能等到程序运行结束由操作系统回收。为了避免内存泄露,通常 new 和 delete、new[] 和 delete[] 操作符应该成对出现,并且不要和C语言中 malloc()、free() 一起混用。
在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,最明显的是可以自动调用构造函数和析构函数。
9.C++inline内联函数
函数调用是有时间和空间开销的。程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码;函数体中的代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码。
为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数。
指定内联函数的方法很简单,只需要在函数定义处增加 inline 关键字。
#include <iostream>
using namespace std;
//内联函数,交换两个数的值
inline void swap(int *a, int *b){
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main(){
int m, n;
cin>>m>>n;
cout<<m<<", "<<n<<endl;
swap(&m, &n);
cout<<m<<", "<<n<<endl;
return 0;
}
要在函数定义处添加 inline 关键字,在函数声明处添加 inline 关键字虽然没有错,但这种做法是无效的,编译器会忽略函数声明处的 inline 关键字。
使用内联函数的缺点也是非常明显的,编译后的程序会存在多份相同的函数拷贝,如果被声明为内联函数的函数体非常大,那么编译后的程序体积也将会变得很大,所以再次强调,一般只将那些短小的、频繁调用的函数声明为内联函数。
10.C++内联函数也可以用来代替宏
内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误。这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏。
11.规范使用C++内联函数
inline 关键字可以只在函数定义处添加,也可以只在函数声明处添加,也可以同时添加;但是在函数声明处添加 inline 关键字是无效的,编译器会忽略函数声明处的 inline 关键字。也就是说,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
更为严格地说,内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格。
在多文件编程中,我们通常将函数的定义放在源文件中,将函数的声明放在头文件中,希望调用函数时,引入对应的头文件即可,我们鼓励这种将函数定义和函数声明分开的做法。但这种做法不适用于内联函数,将内联函数的声明和定义分散到不同的文件中会出错。
内联函数虽然叫做函数,在定义和声明的语法上也和普通函数一样,但它已经失去了函数的本质。函数是一段可以重复使用的代码,它位于虚拟地址空间中的代码区,也占用可执行文件的体积,而内联函数的代码在编译后就被消除了,不存在于虚拟地址空间中,没法重复使用。
12.C++函数的默认参数
在C++中,定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。
所谓默认参数,指的是当函数调用中省略了实参时自动使用的一个值,这个值就是给形参指定的默认值。
#include<iostream>
using namespace std;
//带默认参数的函数
void func(int n, float b=1.2, char c='@'){
cout<<n<<", "<<b<<", "<<c<<endl;
}
int main(){
//为所有参数传值
func(10, 3.5, '#');
//为n、b传值,相当于调用func(20, 9.8, '@')
func(20, 9.8);
//只为n传值,相当于调用func(30, 1.2, '@')
func(30);
return 0;
}
C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。
默认参数并非编程方面的重大突破,而只是提供了一种便捷的方式。在以后设计类时你将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
13.在声明中还是在定义中指定默认参数
C++ 规定,在给定的作用域中只能指定一次默认参数。
C语言有四种作用域,分别是函数原型作用域、局部作用域(函数作用域)、块作用域、文件作用域(全局作用域),C++ 也有这几种作用域。
在多文件编程时,我们通常的做法是将函数声明放在头文件中,并且一个函数只声明一次,但是多次声明同一函数也是合法的。
在给定的作用域中一个形参只能被赋予一次默认参数。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认值,而且该形参右侧的所有形参必须都有默认值。
#include <iostream>
using namespace std;
//多次声明同一个函数
void func(int a, int b, int c = 36);
void func(int a, int b = 5, int c);
int main(){
func(99);
return 0;
}
这种声明方式是正确的。第一次声明时为 c 指定了默认值,第二次声明时为 b 指定了默认值;第二次声明是添加默认参数。需要提醒的是,第二次声明时不能再次给 c 指定默认参数,否则就是重复声明同一个默认参数。
14.C++函数重载
C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。借助重载,一个函数名可以有多种用途。
参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。
借助函数重载交换不同类型的变量的值。
#include <iostream>
using namespace std;
//交换 int 变量的值
void Swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
//交换 float 变量的值
void Swap(float *a, float *b){
float temp = *a;
*a = *b;
*b = temp;
}
//交换 char 变量的值
void Swap(char *a, char *b){
char temp = *a;
*a = *b;
*b = temp;
}
//交换 bool 变量的值
void Swap(bool *a, bool *b){
char temp = *a;
*a = *b;
*b = temp;
}
int main(){
//交换 int 变量的值
int n1 = 100, n2 = 200;
Swap(&n1, &n2);
cout<<n1<<", "<<n2<<endl;
//交换 float 变量的值
float f1 = 12.5, f2 = 56.93;
Swap(&f1, &f2);
cout<<f1<<", "<<f2<<endl;
//交换 char 变量的值
char c1 = 'A', c2 = 'B';
Swap(&c1, &c2);
cout<<c1<<", "<<c2<<endl;
//交换 bool 变量的值
bool b1 = false, b2 = true;
Swap(&b1, &b2);
cout<<b1<<", "<<b2<<endl;
return 0;
}
重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活。
函数的重载的规则:1.函数名称必须相同,2.参数列表必须不同(个数不同,类型不同,参数排列顺序不同等),3.函数的返回类型可以相同也可以不相同,4.仅仅返回类型不同不足以成为函数的重载。
C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)
会被重命名为_Swap_int_int
,void Swap(float x, float y)
会被重命名为_Swap_float_float
。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。
从这个角度讲,函数重载仅仅是语法层面的,本质上它们还是不同的函数,占用不同的内存,入口地址也不一样。
15.函数重载过程中的二义性和类型转换
C++ 标准还规定,编译器应该按照从高到低的顺序来搜索重载函数,首先是精确匹配,然后是类型提升,最后才是类型转换;一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。
优先级 | 包含的内容 | 举例说明 |
---|---|---|
精确匹配 | 不做类型转换,直接匹配 | (暂无说明) |
只是做微不足道的转换 | 从数组名到数组指针、从函数名到指向函数的指针、从非 const 类型到 const 类型。 | |
类型提升后匹配 | 整型提升 | 从 bool、char、short 提升为 int,或者从 char16_t、char32_t、wchar_t 提升为 int、long、long long。 |
小数提升 | 从 float 提升为 double。 | |
使用自动类型转换后匹配 | 整型转换 | 从 char 到 long、short 到 long、int 到 short、long 到 char。 |
小数转换 | 从 double 到 float。 | |
整数和小数转换 | 从 int 到 double、short 到 float、float 到 int、double 到 long。 | |
指针转换 | 从 int * 到 void *。 |
16.C++和C混合编程
使用 C 和 C++ 进行混合编程时,考虑到对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。
C++ 给出了相应的解决方案,即借助 extern "C",就可以轻松解决 C++ 和 C 在处理代码方式上的差异性。
extern 是 C 和 C++ 的一个关键字,extern "C" 既可以修饰一句 C++ 代码,也可以修饰一段 C++ 代码,它的功能是让编译器以处理 C 语言代码的方式来处理修饰的 C++ 代码。
对于解决 C++ 和 C 混合编程的问题,通常在头文件中使用如下格式:
#ifdef __cplusplus
extern "C" {
#endif
void display();
#ifdef __cplusplus
}
#endif
extern "C" 大致有 2 种用法,当仅修饰一句 C++ 代码时,直接将其添加到该函数代码的开头即可;如果用于修饰一段 C++ 代码,只需为 extern "C" 添加一对大括号{}
,并将要修饰的代码囊括到括号内即可。