从C到C++

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变成了iostreamfstream.h变成了fstream。而对于原来C语言的头文件,也采用同样的方法,但在每个名字前还要添加一个c字母,所以C语言的stdio.h变成了cstdiostdlib.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_intvoid 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" 添加一对大括号{},并将要修饰的代码囊括到括号内即可。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值