我的c++之旅(极其详细且通俗易懂的c++学习笔记)

这是本人学习c语言中文网中的c++课程所做的笔记,为了防止学了之后忘记,我把每节所讲的内容全都提炼成问题的形式放在最开头,答出问题便是掌握了对应的知识点,我讨厌死记硬背,所以这些问题并没有标准答案,只有包含这些答案的文章,读者理解了文章的含义(我也在尽可能通俗易懂的基础上精简笔记内容)答案自然就可以用自己的话说出来。由于本来是打算自用来着,所以有些地方格式不太恰当,

风起于青萍之上,浪成于微澜之间。加油吧,骚年!你的c++旅途,就此开始!

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第1张图片

 

我的故事

虽然确实想写一些特殊且有趣的经历,但是还是留着以后写吧,现在先忙正事~

我的c++之旅

一些建议:旅途中的所有代码复制到vs中看会舒服一点

如果旅途过程感到太累,不妨歇一歇,慢一点、轻松一点、认真一点、开心一点~

差点忘说了,本文只是第一章,后续会根据学习进度不断更新^_^

前言:什么是oop?

面向对象究竟是什么鬼?该如何理解? (biancheng.net)

  • 程序运行的本质是什么?

  • oop出现前后的区别是什么?

  • 什么是面向对象编程?

  • oop主要目的是什么?

  • 什么是类? 类的作用是什么?

  • c语言为什么被称为面向对象的编程语言?

  • 对象跟基本变量相比的特性是什么?

  • 内存空间qv是什么?

  • c++为什么被称为面向对象编程?

  • 什么是对象?什么是类的实例化?属性、方法是什么?

未使用oop的代码

 #include 
 ​
 //定义变量
 char *name;
 int age;
 float score;
 ​
 //定义函数
 void display(){
     printf("%s的年龄是 %d,高考成绩是 %.1f\n", name, age, score);
 }
 ​
 int main(){
     //为变量赋值
     name = "严长生";
     age = 32;
     score = 622;
     //调用函数
     display();
     }
 运行结果:
 严长生的年龄是 32,高考成绩是 622.0
     

使用了 OOP 的 C++ 代码:

 #include 
 ​
 //通过class关键字类定义类
 class Student{
 public:
     //类包含的变量
     char *name;
     int age;
     float score;
     //类包含的函数
     void display(){
         printf("%s的年龄是 %d,高考成绩是 %.1f\n", name, age, score);
     }
 };
 ​
 int main(){
     //通过类来创建对象
     Student stu1;
     //为类中的变量赋值
     stu1.name = "严长生";
     stu1.age = 32;
     stu1.score = 622;
     //调用类中的函数
     stu1.display();
 ​
     return 0;
 }

在编程语言中,变量看做数据,用来存储多种形式的值。函数看做操作,用来对数据进行某些处理,所有代码都由数据和操作构成,程序运行的本质就是对数据进行各种各样的操作。

oop出现以前:数据和操作是分离的,我们无法将数据和操作在语法上聚合在一起;当代码量暴增以后,我们很难搞清楚哪份数据和哪个操作相关联,导致逻辑非常混乱,不利于大规模程序开发。

数据直接暴露在全局范围内,和A操作有关的数据也可以在b操作中使用,有可能导致程序员错误地使用数据。

oop出现以后:我们就可以把相关的数据和操作放在一个容器中,这个容器就是类(class)

类的作用:类把相关的变量和函数封装起来(使他们在语法上相关联),并且与其他类中的变量和函数相隔离,避免程序员误操作

OOP 其实就是一种代码封装思想,它将相关变量和函数放到一个类中,并将它们保护和隔离起来,形成一个一个的小模块,每个小模块能够完成一个小任务。

OOP 在代码执行效率上绝对没有任何优势

oop主要目的:方便程序员组织和管理代码,快速梳理编程思路

:一种复杂的、由用户自己构建的数据类型。它和int float double string 等基本类型一样都必须定义出变量才能用,只不过通过类定义出的变量有了一个新名字--"对象"

对象的新特性:可以使用内部的普通变量和函数

内存空间:类只是一张图纸,不占用内存空间,对象是具体的零件,需要地方存放,占用内存空间

C语言因为不支持类和对象的概念,被称为面向过程的编程语言

在c++中,通过类名创建对象,这个过程叫做类的实例化(即按图纸生产零件),因此也称类是对象的一个实例(Instance)。类的成员变量称为属性,类的成员函数称为方法

相比面向过程,面向对象增加了一层封装,就是类。不要小看类(Class)这一层封装,它有很多特性,极大地方便了中大型程序的开发。

1.从c到c++

1.1 c++和c语言的关系

学习C++之前要先学习C语言吗? (biancheng.net)

  • c++的含义

  • 简述c++的发展史 早期 流行

  • 为什么我们很难说 C++ 拥有独立的编译器

  • 什么是c/c++编译器?

  • 作者的建议是什么?

C++ 读作“C加加”,是“C Plus Plus”的简称。顾名思义,C++ 是在C语言的基础上增加新特性,玩出了新花样。

学了C语言就相当于学了 C++ 的一半,从C语言转向 C++ 时,不需要再从头开始,接着C语言往下学就可以,所以我强烈建议先学C语言再学 C++。

C++和C语言的血缘关系

早期并没有“C++”这个名字,而是叫做“带类的C”。作为C语言的一个扩展和补充出现,它增加了很多新的语法,目的是提高开发效率。

仅支持简单的面向对象编程,也没有自己的编译器,而是通过一个预处理程序(名字叫 cfront),先将 C++ 代码”翻译“为C语言代码,再通过C语言编译器合成最终的程序。

随着 C++ 的流行,它的语法也越来越强大,已经能够很完善的支持面向过程编程、面向对象编程(OOP)和泛型编程,几乎成了一门独立的语言,拥有了自己的编译方式。

我们很难说 C++ 拥有独立的编译器,例如 Windows 下的微软编译器(cl.exe)、Linux 下的 GCC 编译器、Mac 下的 Clang 编译器(已经是 Xcode 默认编译器,雄心勃勃,立志超越 GCC),它们都同时支持C语言和 C++,统称为 C/C++ 编译器对于C语言代码,它们按照C语言的方式来编译;对于 C++ 代码,就按照 C++ 的方式编译。

从表面上看,C、C++ 代码使用同一个编译器来编译,所以上面我们说“后期的 C++ 拥有了自己的编译方式”,而没有说“C++ 拥有了独立的编译器”。

作者建议

没有任何编程基础的读者,我建议先从C语言学起,不要贪多嚼不烂。有编程基础的读者,相信你自己能做出正确的判断。

学习编程是一个循序渐进的过程,不要期望一口吃个胖子。学习C语言,一来是学习它的语法,为 C++ 打基础,同时培养编程兴趣,二来是学习内存、编译和链接,弄清编程语言的内在机理。

每个初学者都经历过这样的窘境:已经学习了语法,明白了编程语言都有什么,也按照教程敲了不少代码,但是遇到实际问题就挂了,没有思路,不知道从何下手。说白了就是只会学不会用。

究其原因,就是实践少,没有培养起编程思维!学习知识容易,运用知识难!

等你熟悉了C语言,能编写出上百行的代码,就对编程有些概念了。这个时候再去了解 C++ 究竟在C语言基础上增加了什么,你就站在了一定的高度。

从“学院派”的角度来说,C++ 支持面向过程编程、面向对象编程和泛型编程,而C语言仅支持面向过程编程。就面向过程编程而言,C++ 和C几乎是一样的,所以学习了C语言,也就学习了 C++ 的一半,不需要从头再来。

1.2C++类和对象到底是什么意思?

C++类和对象到底是什么意思?

C++类和对象到底是什么意思?

  1. 类和结构体的关系是什么?

  2. 什么是结构体?

  3. 什么是类?

  4. 什么是对象?

  5. 结构体和类的异同?

  6. 什么是类的实例化?

  7. 图示c、c++中项目的组成方式

  8. 面向对象和面向过程的关系

C++ 中的类(Class)可以看做C语言中结构体(Struct)的升级版。

结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。

C++ 中的类也是一种构造类型,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数;通过类定义出来的变量也有特定的称呼,叫做“对象(Object)”。

结构体和类都可以看做一种由用户自己定义的复杂数据类型,在C语言中可以通过结构体名来定义变量,在 C++ 中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)

在 C++ 中,通过类名创建对象,即将图纸生产成零件,这个过程叫类的实例化,因此也称对象是类的一个实例(Instance)。

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第2张图片

图1:C语言中项目的组织方式

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第3张图片

图2:C++中项目的组织方式

面向对象编程是针对开发中大规模的程序而提出来的,目的是提高软件开发的效率。不要把面向对象和面向过程对立起来,面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。

代码示例:

struct

 #include 
 ​
 //定义结构体 Student
 struct Student{
     //结构体包含的成员变量
     char *name;
     int age;
     float score;
 };
 //显示结构体的成员变量
 void display(struct Student stu){
     printf("%s的年龄是 %d,成绩是 %f\n", stu.name, stu.age, stu.score);
 }
 ​
 int main(){
     struct Student stu1;
     //为结构体的成员变量赋值
     stu1.name = "小明";
     stu1.age = 15;
     stu1.score = 92.5;
     //调用函数
     display(stu1);
 ​
     return 0;
 }

class

#include 

//通过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 = 15;
    stu1.score = 92.5f;
    //调用类的成员函数
    stu1.say();

    return 0;
}

1.3如何编译和运行c++程序

  1. 图示c/c++代码生成可执行文件的过程

  2. c与c++后缀区别

  3. *不同编译器以及其支持的后缀

  4. IDE含义是什么?俗称什么?

  5. 如何编译c++程序?c/c++IDE,LinuxGCC

  6. 如何使用g++命令编译c++源文件?

  7. *简述GCC历史

下图是 C/C++ 代码生成可执行文件的过程:

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第4张图片

C语言源文件的后缀非常统一,在不同的编译器下都是.c。C++ 源文件的后缀则有些混乱,不同的编译器支持不同的后缀

推荐使用.cpp作为 C++ 源文件的后缀,这样更加通用和规范。

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第5张图片

IDEIntegrated Development Environment 的缩写,中文称为集成开发环境,用来表示辅助程序员开发的应用软件,是它们的一个总称。Integrated集成的 综合的。

集成开发环境就是一系列开发工具的组合套装

运行 C 语言(或 Java 语言)程序必须有编译器,而运行 Python 语言程序必须有解释器。在实际开发中,除了运行程序必须的工具外,我们往往还需要很多其他辅助软件,例如语言编辑器、自动建立工具、除错器等等。这些工具通常被打包在一起,统一发布和安装,例如 PythonWin、MacPython、PyCharm 等,它们统称为集成开发环境(IDE)

为了称呼方便,人们也常常会将集成开发环境称为编译器或编程软件,对此读者没必要较真儿,就把它当做“乡间俗语”吧。

虽然有些IDE支持多种程序语言的开发(如 Eclipse、NetBeans、VS)但通常情况下,IDE还是针对某一特定的程序语言量身打造的

编译 C++ 程序只需要把源文件的后缀设置为.cpp

g++ 命令

Linux GCC编译c/c++文件:

gcc main.c//编译单个c源文件
gcc main.c module.c//多个
gcc main.cpp -lstdc++//编译单个c++源文件*
gcc main.cpp module.cpp -lstdc++//多个*

GCC 中还有一个g++命令,它专门用来编译 C++ 程序,广大 C++ 开发人员也都使用这个命令。(用这个)

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第6张图片

1.4 C++命名空间

C++命名空间

  1. 为什么c++引入命名空间(namespace)

  2. 命名空间别称是什么?

  3. namespce 语法格式,如何定义一个命名空间?如何使用命名空间中的名字?

  4. ::符号叫什么?怎么用(两种)?

  5. using声明符的用法是什么?

  6. 命名空间内部到底能做什么?

  7. 站在编译和链接的角度,代码中出现的各种名称(变量名、函数名)到底代表什么?

  8. 自己写一段命名空间完整示例代码

为了解决合作开发时的命名冲突问题,C++ 引入了命名空间(Namespace)的概念。

namespace Li{  //小李的变量定义
    FILE *fp=NULL;
}
namespace Han{  //小韩的变量定义
    FILE *fp=NULL;
}

命名空间有时也被称为名字空间、名称空间

namespace是c++中的关键字,用它来定义一个命名空间,语法格式为

namespace name{
	变量/函数/类/typedef/#define
}

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

使用变量、函数时要指明它们所在的命名空间。

法一:

Li::fp=fopen("c:users//temp","t")//使用小李定义的变量fp
Han::dp=fopen(""c:users//temp","t"")//使用小韩定义的变量 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

如果有未具体指定命名空间的变量产生了命名冲突,默认采用命名空间 Li 中的变量

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

命名空间内可以声明定义 变量、函数、类、typedef、#define,以及所有能在namespace外声明定义的名称

站在编译和链接的角度,变量名,函数名,类名都是一种符号,有的符号可以指代一个内存位置(比如int a = 4,a占了4个字节),有的符号仅仅是一个名称,例如 typedef 定义的类型别名。

命名空间完整示例代码:

#include 

//将类定义在命名空间中
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;
}

运行结果: 小明的年龄是 15,成绩是 92.500000

自己调试:

#include 

//将类定义在命名空间中
namespace Diy{
     class Student{
    public:
        char *name;
        int age;
        float score;
  
    public:
        void say(){
            printf("%s的年龄是 %d,成绩是 %f\n", name, age, score);
        }
    };//分号不能省略 student直接定义不用class student 
    class Student stu2; 
    typedef Student Student2;//注意,如果在定义class时就typedef 重命名程序会报错 
}

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

    return 0;
}

1.5 C++头文件和std命名空间(精辟)

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

  1. 简述c++标准命名空间是怎样产生的(历史)?

  2. 引入命名空间后c++开发人员如何做到在兼容旧版本的基础上推出新的c++标准?又如何将c语言的头文件、c++未引入命名空间的头文件纳入命名空间std的?

  3. 旧的c++头文件现在还能用吗?

  4. c++头文件现状是什么?

  5. .h头文件和不带.h头文件使用区别?

  6. 使用using namespace std的风险

  7. #include<>和#include" "的区别(答案是截图)

  8. namespace的特性

  9. 为什么using namespace std和#include都要写呢

    #include只是把你程序种对应代码给替换了,而替换后的代码他的名称都在命名空间std中定义声明的,这个时候如果没有using namespace std,程序根本认不出这个函数

  10. 为什么要尽量使用c++头文件?

  11. using指示的作用域是什么?using指示的作用域_ using声明、using指示用于嵌套命名空间时的作用域C语言中文网说的是所在函数结束时 csdn说是指命名空间所在的最近的外层作用域或using指示所在的最近的外层作用域(两者取其大)。

  12. 较为安全的使用using namespace std的方法(示例代码)是什么?

众所周知,早期c++,没有命名空间,只支持简单的面向对象编程,也没有属于自己的编译方式,而是通过一个预处理程序cfront,将c++代码翻译成c代码,再用c语言编译器执行。

引入命名空间前

早期c++在c语言的基础上开发了一些属于自己的库(头文件)

iostream.h:用于控制台输入输出(现在已经不能用了)。fstream.h:用于文件操作。 complex.h:用于复数计算

依然以.h为后缀,其中包含的函数、类、宏(预处理方式的一种比如#define a 5)都是全局范围的

引入命名空间后

c++计划重新编写库,将原来头文件中的函数、类、宏全部纳入一个新的命名空间“std”(standard的缩写意为“标准命名空间”) 避免在开发时与项目中的函数、类、宏重名,但是老式c++、c程序没有使用命名空间。直接修改原来的库会带来一个很严重的后果:程序员会因为不愿花费大量时间修改老式代码而极力反抗,拒绝使用新标准的 C++ 代码。

解决方法:保留原来的库和头文件,它们在 C++ 中可以继续使用,把原来的库复制一份稍作修改纳入命名空间std,把原来的库复制一份。共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。

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

旧的 C++ 头文件是官方所反对使用的,已明确提出不再支持 但旧的C头文件仍然可以使用,以保持对C的兼容性。

c++头文件现状

  • 旧的c++头文件比如iostream.h继续被支持(通过自己调试发现在devc++中已经不支持了)(这是为了保证过去用旧c++头文件编写的程序依然可以运行),但不在官方标准中头文件的内容不在命名空间 std 中。

  • 新的c++头文件比如iostream和旧的头文件iostream.h虽然细节有所不同,但功能相近但头文件的内容在命名空间 std 中。

  • 原来c语言的头文件stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中。

  • 具有C库功能的新C++头文件如 cstdio、cstdlib 内容和相应的旧的C头文件相同,只是内容在 std 中

对于不带.h的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;对于带.h的头文件,没有使用任何命名空间,所有符号都位于全局作用域(即全局命名空间)。

using namespace std;将域中的变量都释放于全局区(注:释放前受命名空间域的限制)

这种方式虽然最为方便,但危险性也是最高的,不管会不会用到,都会放出来,发生命名冲突的的可能性也更高。

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第7张图片

namespace的特性:嵌套 同名融合 using namespace std;到底是在干嘛?

C++ 新增的库更加强大和灵活,请读者尽量使用这些 C++ 新增的头文件,例如 iostream、fstream、string 等。

较为安全的使用using namespace std

#include 

void func(){
    //必须重新声明
    using namespace std;
    cout<<"http://c.biancheng.net"< 
  
#include 

//声明命名空间std
using namespace std;

void func(){
    cout<<"http://c.biancheng.net"< 
  

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

1.6 C++输入输出(浅)

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

  1. *iostream扩写是什么?

  2. 标准输入,输出;标准错误指什么?

  3. 什么是内置对象?

  4. cout 和 cin 分别是 ostream 和 istream 类的什么?

  5. C++ 库定义了大量的什么?

  6. ">>" "<<"运算符的作用是什么?

  7. *endl与\n endl含义

  8. cin>>x;不能接收空格tab 换行

iostream 是 Input Output Stream 的缩写,意思是“输入输出流”。

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

C++ 使用输入输出时,需要包含头文件iostream,它包含了用于输入输出的对象,例如常见的cin表示标准输入、cout表示标准输出、cerr表示标准错误。

cout 和 cin 都是 C++ 的内置对象,而不是关键字。

在 C++ 中提前创建好的对象称为内置对象

cout 和 cin 就分别是 ostream 和 istream 类的对象它们是由标准库的开发者提前创建好的,可以直接拿来使用。

C++ 库定义了大量的类(Class),程序员可以使用它们来创建对象

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

endl 扩写:“end of line”

    int x;
     cin>>x;// ;用户输入的不是 int 型数据,则会被强制转化为 int 型数据。

1.7C++变量的定义位置

C++变量的定义位置

C89 规定,所有局部变量都必须定义在函数开头

现在变量只要在使用之前定义好即可,不强制必须在函数开头定义所有变量。

for(int i=1; i<=n ;i++)减少命名冲突概率

1.8C++布尔类型(浅)

C++变量的定义位置

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

C++ 新增了 bool 类型(布尔类型),它一般占用 1 个字节长度用法和int char long一样

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

在 C++ 中使用 cout 输出 bool 变量的值时还是用数字 1 和 0 表示,而不是 true 或 false。Java、PHP、JavaScript 等也都支持布尔类型,但输出结果为 true 或 false,

1.9 C++中的const又玩出了新花样

C++中的const又玩出了新花样

c中const的用法

  1. C中的const的用法(全称是什么?变量命名建议,2种定义方式,初始化常量可以使用什么?2种初始化时间)

  2. const 和指针变量一起使用(限制对象是什么?,3种定义顺序及不同作用,如何同时限制指针变量本身和指针变量指向的数据,记忆诀窍是什么?)

  3. const 和函数形参(const相比#define的优势,怎么用?哪里用到了?优点是什么?)

  4. const 和非 const 类型转换(产生原因是什么?编译器采取了什么措施?自己调试后发现的原因是什么?编译器的转换规则是什么?为什么要设立这样的转换规则?,这一规则在c标准库中的应用 。注意:传参的本质也是赋值,不能将const限定的变量赋值到函数中(代码示例))

  5. c++中的const

1.

有时候我们希望定义这样一种变量,它的值不能被改变,在整个作用域中都保持固定。为了满足这一要求,可以使用const关键字对变量加以限定。

建议将常量名的首字母大写,以提醒程序员这是个常量。

我们经常将 const 变量称为常量(Constant)

创建常量的格式

const type name = value;//推荐使用
type const name = value;
const int MaxNum = 100;  //班级的最大人数

常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误。初始化常量可以使用任意形式的表达式

2种初始化时间

#include 

int getNum(){
    return 100;
}

int main(){
    int n = 90;
    const int MaxNum1 = getNum();  //运行时初始化
    const int MaxNum2 = n;  //运行时初始化
    const int MaxNum3 = 80;  //编译时初始化
    printf("%d, %d, %d\n", MaxNum1, MaxNum2, MaxNum3);

    return 0;
}

2.const 和指针变量一起使用,可以限制指针变量本身,也可以限制指针指向的数据

const 和指针一起使用会有3种不同的顺序

const int *p1;//指针所指向的数据是只读的p1、p2 本身的值可以修改
int const *p2;//指针所指向的数据是只读的p1、p2 本身的值可以修改
int * const p3;//指针是只读的,也就是 p3 本身的值不能被修改

指针本身和它指向的数据都有可能是只读的

const int * const p4;
int const * const p5;

const 离变量名近就是用来修饰指针变量的,离变量名远就是用来修饰指针指向的数据,如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

3.单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。

const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

c标准库中的函数就用到了这个

size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, ... );

用 const 加以限制,不但可以防止由于程序员误操作引起的字符串修改,还可以给用户一个提示,函数不会修改你提供的字符串,请你放心。

4.

以下代码如果顺利运行会使不能改变的const char *str1改变

const char *str1=&a;//限制了str1指向数据的值,没有限制str1本身指向地址的值
char*str2=&b;
str1=str2;//此时*str1=*str2,*str1的值改变了

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第8张图片

str1的地址改变了,原本const限制不变的*str1的值也改变了(这样是合法的,下面的不合法

编译器为了避免上述情况,禁止了const char *str1转换为const char 2(规定const char *char *`是不同的类型,第三行编译错误)意思就是const不能给不是const的赋值,但反过来可以

const char *char *是不同的类型,不能将const char *类型的数据赋值给char *类型的变量。但反过来是可以的,编译器允许将char *类型的数据赋值给const char *类型的变量。

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第9张图片

结论:这样规定重点考虑的是保护重要数据(指const的数据只读不能写入)本身不被改变

C语言标准库中很多函数的参数都被 const 限制了,但我们在以前的编码过程中并没有注意这个问题,经常将非 const 类型的数据传递给 const 类型的形参,这样做从未引发任何副作用,原因就是上面讲到的,将非 const 类型转换为 const 类型是允许的。

#include 

void func(char *str){ }

int main(){
    const char *str1 = "c.biancheng.net";
    char *str2 = str1;
    func(str1);
    return 0;
}

第7、8行代码分别通过赋值、传参(传参的本质也是赋值)将 const 类型的数据交给了非 const 类型的变量,编译器不会容忍这种行为,会给出警告,甚至直接报错。

1.10 C++ new和delete运算符简介

C++ new和delete运算符简介

  1. c语言中用来动态分配内存和释放内存的函数是什么?代码示例

  2. c++中新增用来动态分配和释放内存的关键字是什么?优点是什么?

  3. new delete new[] delete[]代码示例

  4. 为什么new delete要成对出现?为什么不能和malloc混用?

  5. 为什么建议使用new?

c语言用来动态分配内存的函数malloc()释放内存用free()

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

c++新增了new和delete函数。new 用来动态分配内存,delete 用来释放内存。

优点:更简单 new操作符会根据其后面的数据类型来推断所需空间的大小

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

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

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

malloc() new 都是在堆区分配内存,必须手动释放,否则只能等到程序运行结束用操作系统回收,为了避免内存泄漏new 和 delete应该成对出现且不要和C语言中 malloc()、free() 混用

在C++中,建议使用 new 和 delete 来管理内存,它们可以使用C++的一些新特性,最明显的是可以自动调用构造函数和析构函数,后续我们将会讲解。

1.11 C++ inline内联函数详解

C++内联函数

  1. 关于函数调用细节请移步《C语言内存精讲》一章中的《一个函数在栈上到底是怎样的》《用一个实例来深入剖析函数进栈出栈的过程》。

  2. 从代码执行的角度讲函数是什么?

  3. 主调函数与被调函数的执行关系是什么?

  4. C/C++ 程序的执行过程是什么?

  5. *函数调用过程简述

  6. inline是因为解决什么问题产生的?

  7. inline目的、原理是什么?

  8. 什么是内联函数?又称什么?

  9. 如何使用inline?(代码示例)以及需要注意的问题(2个)是什么?

  10. 反面教材(代码示例)

  11. 内联函数的缺点是什么?

  12. 内联函数的适用场景是什么?

  13. 最后关于inline函数说明

2.函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码。

CPU 在执行主调函数代码时如果遇到了被调函数,主调函数就会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回到主调函数,主调函数根据刚才的状态继续往下执行。

c/c++程序的执行过程可以被认为是多个函数之间相互调用的过程,它们形成了一个简单或复杂的调用链条,这个链条的起点是main(),终点也是main(),当main()调用完了所有函数,他会返回一个值(eg:return 0),来结束自己的生命,从而结束整个程序。

函数调用是有时间和空间的开销的,程序在执行一个函数之前需要做一些准备工作,要将实参、局部变量、返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码,函数代码执行完毕后还要清理现场,将之前压入栈中的数据都出栈,才能接着执行函数调用位置以后的代码

如果函数体代码比较多,需要较长的执行时间,那么函数调用机制占用的时间可以忽略;如果函数只有一两条语句,那么大部分的时间都会花费在函数调用机制上,这种时间开销就就不容忽视

为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。

这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function)又称内嵌函数或者内置函数。

指定内联函数的方法很简单,只需要在函数定义处增加 inline 关键字。

#include 
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< 
  

注意,要在函数定义处添加inline关键字,在函数声明处添加inline关键字是无效的

程序第 16 行被置换成:

int temp;
temp = *(&m);
*(&m) = *(&n);
*(&n) = temp;

注意**编译器可能会将 (&m)、(&n) 分别优化为 m、n。**

反面教材

#include 
using namespace std;

//声明内联函数
void swap1(int *a, int *b);  //也可以添加inline,但编译器会忽略

int main(){
    int m, n;
    cin>>m>>n;
    cout< 
  

内联函数的缺点也很明显,编译后的程序会存在相当多的函数拷贝,如果被声明为内联函数的函数体很大,那么编译后的程序体积也会变得很大,所以只将那些短小的、频繁调用的函数声明为内联函数。

最后需要说明的是,对函数作 inline 声明只是程序员对编译器提出的一个建议,而不是强制性的,并非一经指定为 inline 编译器就必须这样做。编译器有自己的判断能力,它会根据具体情况决定是否这样做。

1.12内联函数也可以用来代替宏

C++内联函数也可以用来代替宏

  1. 什么是宏?移步c语言知识点

  2. 宏和函数的异同是什么?代码示例及所发现的问题,怎么改正?

  3. 问题二说明了什么?

  4. 如何避免这种问题?代码示例

  5. 在编写c++代码时,推荐用内联函数来代替带参数的宏?

  6. 为什么宏和内联函数可以定义在头文件中(不用加static关键字),且头文件被多次#include后也不会引发重复定义错误?非内联函数又是怎样的?

  7. 内联函数的两个主要作用是什么?哪个更能凸显它的意义?

2.宏是可以带参数的,他在形式上和函数非常相似。区别:宏只是字符串替换不是按值传递。

使用宏的一个经典例子是求一个数的平方,如下所示:

  1. #include

  2. using namespace std;

  3. #define SQ(y) y*y

  4. int main(){

  5. int n, sq;

  6. cin>>n;

  7. sq = SQ(n);

  8. cout<

  9. return 0;

  10. }

运行结果: 9↙ 81

从表面上看这个宏定义是正确的,但当我们将宏调用SQ(n)换成SQ(n+1),就会出现意想不到的状况:

#include using namespace std; #define SQ(y) y*y int main(){ int n, sq; cin>>n; sq = SQ(n+1); cout<

运行结果: 9↙ 19

正确结果100 这里输出19,答案错误,这是因为宏展开仅仅是字符串的替换,不会进行任何计算和传值,sq = SQ(n+1);在宏展开后会变为`sq = n+1*n+1;

想得到正确的结果,还应该对宏加以限制,在两边增加( ),如下所示:

#define SQ(y) ( (y)*(y) )

3.说了这么多,我最终想强调的是,宏定义是一项“细思极密”的工作,一不小心就会踩坑,而且不一定在编译和运行时发现,给程序埋下隐患

4.用内联函数代替宏定义,情况就没有那么复杂了,程序员就会游刃有余。

#include using namespace std; inline int SQ(int y){ return y*y; } int main(){ int n, sq; cin>>n; //SQ(n) sq = SQ(n); cout<sq = SQ(n+1); cout<

运行结果: 9↙ 81 100 2

看,一切问题迎刃而解!发生函数调用时,编译器会先对实参进行计算,再将计算的结果传递给形参,并且函数执行完毕后会得到一个值,而不是得到一个表达式,这和简单的字符串替换相比省去了很多麻烦,所以在编写C++代码时我推荐使用内联函数来替换带参数的宏。

和宏一样,内联函数可以定义在头文件中(不用加 static 关键字),并且头文件被多次#include后也不会引发重复定义错误。这一点和非内联函数不同,非内联函数是禁止定义在头文件中的,它所在的头文件被多次#include后会引发重复定义错误。

原因:内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误。这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏

综合本节和上节的内容,可以看到内联函数主要有两个作用,一是消除函数调用时的开销,二是取代带参数的宏。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。

1.13 如何规范的使用内联函数?

如何规范地使用C++内联函数

  1. 为什么说inline是一种“用于实现的关键字”而不是一种“用于声明的关键字”?

  2. 为什么虽然inline出现在函数声明处不影响函数功能,却依然不建议这样做呢?

  3. 高质量c++程序设计风格的一个基本原则是什么?

  4. 多文件编程中函数的定义和声明应该怎么存放?内联函数为什么不适用?

  5. 如果不懂编译和链接的过程 请移步C语言头文件的编写

  6. 问题四的代码示例和具体错误分析

  7. 为什么说内联函数已经失去了函数的本质?

  8. 对虚拟地址空间或代码区的概念不了解,阅读《C语言内存精讲》

  9. 在多文件编程中,内联函数的定义和声明应该放在哪里?

  10. 内联函数怎么用更靠谱?

inline 关键字可以只在函数定义处添加,也可以只在函数声明处添加,也可以同时添加;但是在函数声明处添加 inline 关键字是无效的,编译器会忽略函数声明处的 inline 关键字。

尽管大多数教科书中在函数声明和函数定义处都增加了 inline 关键字,但我认为 inline 关键字不应该出现在函数声明处。这个细节虽然不会影响函数的功能,但是体现了高质量 C++ 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

更为严格地说,内联函数不应该有声明,应该将函数定义放在本应该出现函数声明的地方,这是一种良好的编程风格。

在多文件编程中,我们通常将函数的定义放在源文件中,将函数的声明放在头文件中,希望调用函数时,引入对应的头文件即可,我们鼓励这种将函数定义和函数声明分开的做法。但这种做法不适用于内联函数,将内联函数的声明和定义分散到不同的文件中会出错

main.cpp 代码:

#include 
using namespace std;
//内联函数声明
void func();
int main(){
    func();
    return 0;
}

module.cpp 代码:

#include 
using namespace std;
//内联函数定义
inline void func(){
    cout<<"inline function"< 
  

上面的代码能够正常编译,但在链接时会出错。func()是内联函数,编译期间会用它来替换函数调用处,编译完成后函数就不存在了,链接器在将多个文件合并成一个可执行文件时找不到func()函数的定义,所以会产生链接错误。

内联函数虽然叫做函数,在定义和声明的语法上也和普通函数一样,但它已经失去了函数的本质。函数是一段可以重复使用的代码,它位于虚拟地址空间中的代码区,也占用可执行文件的体积,而内联函数的代码在编译后就被消除了,不存在于虚拟地址空间中,没法重复使用。

在多文件编程时,我建议将内联函数的定义直接放在头文件中,并且禁用内联函数的声明(声明是多此一举)。我的理解:放在头文件中,这样每个源文件在编译时都会把内联函数重新定义一遍,不会在某个源文件编译完成后内联函数的定义的代码消失,导致链接器找不到内联函数的定义而链接错误

内联函数看起来简单,但是有很多细节需要注意,从代码重复利用的角度讲,内联函数已经不再是函数了。我认为将内联函数作为带参宏的替代方案更为靠谱,而不是真的当做函数使用。

1.14 C++函数的默认参数详解

C++函数的默认参数

  1. 什么是默认参数?通俗的说 *什么是实参? *什么是形参?

  2. 如何构造带默认参数的函数?代码示例

  3. 默认参数可以用什么指定?

  4. 构造默认参数时要注意什么?为什么?代码示例

  5. 实参和形参的匹配方式是什么?

  6. *默认参数的意义是什么? *设计类时的作用?

  7. 默认参数除了在函数定义时指定还能在什么时候指定?

实参(argument)——全称为"实际参数"是在调用时传递给函数的参数。 实参可以是常量、变量、表达式、函数等,

形参(parameter)——全称为"形式参数" ,定义函数时使用的参数,不实际存在,

实参的个数,类型应与形参一一对应,实参必须要有确定的值。

所谓默认参数,就是当函数调用中省略了实参时自动使用的一个值,这个值就是给形参指定的默认值。

通俗讲:在C++中,定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。如果用户指定了参数的值,那么就使用用户指定的值,

#include using namespace std;

//带默认参数的函数 void func(int n, float b=1.2, char c='@'){ cout<

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;

}

运行结果: 10, 3.5, # 20, 9.8, @ 30, 1.2, @

为参数指定默认值非常简单,直接在形参列表中赋值即可,与定义普通变量的形式类似。

默认参数除了使用数值常量指定,也可以使用表达式指定例如:

float d = 10.8;
void func(int n, float b=d+2.9, char c='@'){
    cout< 
  

c++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。

实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。

正确写法:

void func(int a, int b=10, int c=20){ }
void func(int a, int b, int c=20){ }

错误写法:

void func(int a, int b=10, int c=20, int d){ }
void func(int a, int b=10, int c, int d=20){ }

默认参数并非编程方面的重大突破,而是提供了一种便捷的方式,设计类时,使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。

我们在函数定义处指定了默认参数。除了函数定义,你也可以在函数声明处指定默认参数。

1.15 到底在声明中还是定义中指定默认参数

到底在什么地方指定默认参数

  1. 函数声明的意义是什么?代码示例

  2. 指定默认参数时的两种情况(在函数定义时指定,在函数声明时指定)代码示例

  3. 为什么会出现问题二的情况呢?这里涉及到了《C语言多文件编程》

  4. *c、c++有哪几种作用域?

  5. 默认参数的值同时在函数定义时指定和在函数声明时指定可以分别指定不同的值吗?为什么?

  6. 多文件编程函数的声明应该放在哪里?

  7. 多次声明同一个函数是合法的吗?多次定义同一个函数是合法的吗?

  8. 通过多次声明的方式来给形参赋予默认参数

  9. 自己代码调试

#include 
using namespace std;
void func(int a, int b = 10, int c = 36);//函数的声明,如果去掉这句编译会报错
int main() {
    func(99);
    return 0;
}
void func(int a, int b = 10, int c = 36) {
    cout << a << ", " << b << ", " << c << endl;//函数的定义
}

如果去掉第3行:

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第10张图片

你也可以在函数声明处指定默认参数。不过当出现函数声明时情况会变得稍微复杂,有时候你可以在声明处和定义处同时指定默认参数,有时候你只能在声明处指定,

示例1

#include 
using namespace std;
void func(int a, int b = 10, int c = 36);
int main(){
    func(99);
    return 0;
}
void func(int a, int b = 10, int c = 36){
    cout< 
  

编译时会报错,错误信息表明不能在函数定义和函数声明中同时指定默认参数。

对代码稍作修改,将 func() 函数的定义放到其他源文件中,即可运行

示例2

main.cpp 代码:

#include 
using namespace std;
void func(int a, int b = 10, int c = 36);
int main(){
    func(99);
    return 0;
}

module.cpp 代码:

#include 
using namespace std;
void func(int a, int b = 10, int c = 36){
    cout< 
  

运行结果: 99, 10, 36

示例一错误原因:

这是因为c++规定,在给定的作用域只能指定一次默认参数

示例一:func()函数的定义和声明都在一个源文件,它们的作用域也都是整个源文件,这样就导致,在同一个文件作用域中指定了两次默认参数,违反了c++的规定。

示例二:func() 的声明位于main.cpp,作用域也是main.cpp,而 func() 的定义位于module.cpp,作用域也是module.cppfunc() 的声明和定义位于不同的作用域,相互之间不影响。

C语言有四种作用域,分别是函数原型作用域、局部作用域(函数作用域)、块作用域、文件作用域(全局作用域),C++ 也有这几种作用域。

将 func() 定义处 b、c 的默认值分别设置为 5、57,而声明处 b、c 的默认值不变,依然为 10、36。编译并运行程序,发现输出结果与上面一样,这说明编译器使用的是当前作用域中的默认参数。站在编译器的角度看,他不管当前作用域中是函数声明还是函数定义,只要有默认参数就可以使用,但是在给定的作用域中一个形参只能被赋予一次默认参数

多次定义同一个函数不合法

在多文件编程时,我们通常的做法是将函数声明放在头文件中,并且一个函数只声明一次,但是多次声明同一函数也是合法的。

函数的后续声明只能为之前那些没有默认值的形参添加默认值,而且该形参右侧的所有形参必须都有默认值。

通过多次声明的方式来给形参赋予默认参数

#include 
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++之旅(极其详细且通俗易懂的c++学习笔记)_第11张图片

情况一:

情况:二:

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第12张图片

1.16 C++函数重载详解

C++函数重载详解

  1. c++函数重载是为了解决什么问题而诞生的?c语言支持函数重载吗?c的弊端是什么?

  2. 什么是函数的重载(Function Overloading)

  3. 参数列表不同指什么?*又叫什么?

  4. 重载函数的代码示例 为什么要使用Swap而不是swap?

  5. 重载函数的范围是什么?重载的结果是什么?在中大型项目中的好处构建重载函数时需要注意什么问题?

  6. 函数重载的规则和构建重载函数与要注意的点是什么?

  7. c++是如何做到函数重载的?

  8. 什么是重载决议(Overload Resolution)?

在实际开发中有时候我们需要实现几个功能类似的函数,只是有些细节有所不同

例如希望交换两个变量的值,这两个变量有多种类型,可以是 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++允许函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)借助重载,一个函数名可以有多种用途。

参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。

#include 
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< 
  

运行结果: 200, 100 56.93, 12.5 B, A 1, 0

本例之所以使用Swap这个函数名,而不是使用swap,是因为 C++ 标准库已经提供了交换两个变量的值的函数,它的名字就是swap,位于algorithm头文件中,为了避免和标准库中的swap冲突,本例特地将S大写。

重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。

重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活。

使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人觉得莫名其妙。

函数的重载的规则:

  • 函数名称必须相同。

  • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。

  • 函数的返回类型可以相同也可以不相同。

  • 仅仅返回类型不同不足以成为函数的重载

注意,参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值也不能作为重载的依据。

C++ 是如何做到函数重载的

C++代码在编译时会根据参数列表对函数进行重命名,例如void Swap(int a, int b)会被重命名为_Swap_int_intvoid Swap(float x, float y)会被重命名为_Swap_float_float。当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(Overload Resolution)。

1.17 C++函数重载过程中的二义性和类型转换

函数重载过程中的二义性和类型转换

  1. 上一节最后我们讲到重载裁定(调用重载函数时,编译器会根据传入的实参的类型、数量、顺序逐个匹配确定对应的函数,匹配失败,编译器会报错)少部分例外情况是什么?请举例说明

  2. 正常代码与问题代码示例 为什么编译会报错?(浅说) 如果不是重载函数会怎么样?

  3. 为什么不适合重载函数?重载决议的规则到底是什么?(表格)这个表我们需要记什么?

  4. c++规定编译器应该按照怎样的顺序搜索重载函数?

  5. 什么是函数重载过程中的二义性错误?

  6. 运用重载决议的规则详细说说为什么问题2中的代码示例二会报错?

  7. 类型转换和类型提升的区别是什么?为什么说类型转换是不得已而为之?

  8. 多个参数时的重载裁决规则是什么?代码示例及分析(分析很长但不难心里会就行)

  9. 99.5是什么数据类型?(自己调试的图可以不看,看结论就行)

重载决议(Overload Resolution)在大部分情况下都能精确匹配,当实参的类型和形参的类型不一致时情况就会变得稍微复杂,

例如函数形参的类型是int,调用函数时却将short类型的数据交给了它,编译器就需要先将short类型转换为int类型才能匹配成功。

#include 
using namespace std;
//1号函数
void func(char ch) {
    cout << "#1" << endl;
}
//2号函数
void func(int n) {
    cout << "#2" << endl;
}
//3号函数
void func(long long m) {
    cout << "#3" << endl;
}
//4号函数
void func(double f) {
    cout << "#4" << endl;
}
int main() {
    short s = 99;
    float f = 84.6;

    func('a');  //不需要类型转换,调用func(char)
    func(s);  //将short转换成int,调用func(int)
    func(490000);  //不需要类型转换,调用func(int)
    func(2147483649);//超过了int最大范围2147483647调用func(long)
    func(f);  //将float转换成double,调用func(double)
    return 0;
}

运行结果:

#1
#2
#2
#3
#4

这段代码很容易理解,相信大家都不会有什么疑问。对代码稍作修改,将2号函数void func (int n)去掉

#include 
using namespace std;
//1号函数
void func(char ch){
    cout<<"#1"< 
  

编译时发生错误

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第13张图片

大概的意思是:func(s)func(49)这两个函数发生调用错误,它们可以匹配三个重载函数中的任何一个,编译器不知道如何抉择。

如果func不是重载函数根据以往的编程经验,s和49都会被转换成long类型,从而匹配3号函数void func(long m)中,

*优先级* *包含的内容* *举例说明*
精确匹配 不做类型转换,直接匹配 (暂无说明)
只是做微不足道的转换 从数组名到数组指针、从函数名到指向函数的指针、从非 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 *。

此表记住精确匹配和类型提升的例子就可以了

类型提升只有上表中列出的几种情况,其他情况都是类型转换。

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第14张图片

c++标准还规定,编译器应该按照从高到低的顺序来搜索重载函数,首先是精确匹配,然后是类型提升,最后才是类型转换。一旦在某个优先级中找到唯一的一个重载函数就匹配成功,不再继续往下搜索。

如果在一个优先级中找到多个(两个以及以上)合适的重载函数,编译器就会陷入两难境地,不知道如何抉择,这就是函数重载过程中的二义性错误。

在例2中,func(s)func(49)没有精确匹配的重载函数,将它们的类型都提升为 int 后仍然不能匹配,接下来进入自动类型转换阶段,发现 s 被转换为 char(整型转换)、long(整型转换)、double(整数和小数转换)后都有比较合适的函数,而且它们在同一个优先级中,谁也不比谁优秀,调用哪个都一样,产生了二义性,所以编译器会报错。

注意,类型提升和类型转换不是一码事!类型提升是积极的,是为了更加高效地利用计算机硬件,不会导致数据丢失或精度降低;而类型转换是不得已而为之,不能保证数据的正确性,也不能保证应有的精度。类型提升只有上表中列出的几种情况,其他情况都是类型转换。

多个参数时的二义性

当重载函数有多个参数时也会产生二义性,而且情况更加复杂。C++ 标准规定,如果有且只有一个函数满足下列条件,则匹配成功

  • 该函数对每个实参的匹配都不劣于其他函数;

  • 至少有一个实参的匹配优于其他函数。

函数原型

void func(int, int);  //①
void func(char, int, float);  //②
void func(char, long, double);  //③

分析如下调用

short n = 99;
func('@', n, 99);
func('@', n, 99.5);

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第15张图片

//以下分析可以不看,知道是咋回事就行

\1) 先来看第一个函数调用。如果只考虑第一个实参'@',那么②③两个函数都能够精确匹配,谁也不比谁优秀,是平等的;如果只考虑第二个实参n,对于②,需要把 short 提升为 int(类型提升),对于③,需要把 short 转换为 long(类型转换),类型提升的优先级高于类型转换,所以②胜出;如果只考虑第三个实参99,②③都要进行类型转换,没有哪一个能胜出,它们是平等的。

从整体上看,②③在第一、三个实参的匹配中是平等的,但②在第二个实参的匹配中胜出,也就是说,②对每个实参的匹配都不劣于③,但有一个实参的匹配优于③,所以②最终脱颖而出,成为被调用函数。

\2) 再来看第二个函数调用。只考虑第一个实参时②③是平等的,没有谁胜出;只考虑第二个实参时②胜出;只考虑第三个实参时,②需要类型转换,③能够精确匹配,精确匹配的优先级高于类型转换,所以③胜出。

从整体上看,②③在第一个实参的匹配中是平等的,②在第二个实参的匹配中胜出,③在第三个实参的匹配中胜出,它们最终“打成了平手”,分不清孰优孰劣,所以编译器不知道如何抉择,会产生二义性错误。

//

疑惑所在:只考虑第三个实参时,②需要类型转换,③能够精确匹配,精确匹配的优先级高于类型转换,所以③胜出。

也就是说99.5是double类型!

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第16张图片

实际上是用户未指定数据类型时编译器为确保精度默认给它赋予了double型我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第17张图片

1.18如何实现C++和C的混合编程?

如何实现C++和C的混合编程?

  1. 为什么我们要掌握c++和c混合编程?

  2. c++可以和c混合编程吗?我们需要注意什么问题?

  3. 实例项目所出现的问题是什么?产生问题的原因是什么?解决方法是什么?

  4. 什么是extern"C",什么是extern?两者区别是什么?extern"C"功能是什么?

  5. 再次说明实例项目 程序进行预处理操作时会做什么?如何使用extern"C"解决错误?

  6. 在实际开发中如何解决c++和c的混合编程问题?(代码)

  7. extern"C"的两个用法是什么?

在 C++ 出现之前,很多实用的功能都是用 C 语言开发的,很多底层的库也是用 C 语言编写的。这意味着,如果能在 C++ 代码中兼容 C 语言代码,无疑能极大地提高 C++ 程序员的开发效率。

C++ 和 C 可以进行混合编程。但需要注意的是,由于C++ 和 C 在程序的编译、链接等方面都存在一定的差异,而这些差异往往会导致程序运行失败。

 //myfun.h
 void display();

//myfun.c
#include 
#include "myfun.h"
void display(){
printf("C++:http://c.biancheng/net/cplus/");
}

 //main.cpp
#include 
#include "myfun.h"
using namespace std;
int main(){
	display();‘
	return 0;
 }

分析:主程序用c++编写,display()用c语言编写,分别存为两个源文件.c和.cpp,

运行结果:

In function `main': undefined reference to `display()'

编译器无法找到 main.cpp 文件中 display() 函数的实现代码。

导致此错误的原因,就是因为 C++ 和 C 编译程序的方式存在差异

在1.16中我们知道,c++之所以支持函数重载是因为为 C++ 会在程序的编译阶段对函数的函数名进行“再次重命名”,例如:void Swap(float x, float y) 会被重命名为_Swap_float_float。

但是,C 语言是不支持函数重载的,它不会在编译阶段对函数的名称做较大的改动

C 语言的标准对它们进行编译,函数的函数名将是_Swap。

/*可以不看

不同的编译器有不同的重命名方式,根据 C++ 标准编译后的函数名几乎都由原有函数名和各个参数的数据类型构成,而根据 C 语言标准编译后的函数名则仅有原函数名构成。这里仅仅举例说明,实际情况可能并非如此。这也就意味着,使用 C 和 C++ 进行混合编程时,考虑到对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。

*/

解决方法:

浅层:如果你把它存为两个.cpp可以顺利运行

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第18张图片

深层:

extern "C",就可以轻松解决 C++ 和 C 在处理代码方式上的差异性。

extern是c和c++的一个关键字,,但对于 extern "C",读者大可以将其看做一个整体,和extern毫无关系。

extern "C" 既可以修饰一句 C++ 代码,也可以修饰一段 C++ 代码,它的功能是让编译器以处理 C 语言代码的方式来处理修饰的 C++ 代码。

main.cpp 和 myfun.c 文件中都包含 myfun.h 头文件,当程序进行预处理操作时,myfun.h 头文件中的内容会被分别复制到这 2 个源文件中。

为了避免 display() 函数以不同的编译方式处理,我们应该使其在 main.cpp 文件中仍以 C 语言代码的方式处理,这样就可以解决函数名不一致的问题。

#ifdef __cplusplus
extern "C" void display();
#else
void display();
#endif

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第19张图片

在实际开发中,对于解决 C++ 和 C 混合编程的问题,通常在头文件中使用如下格式:

  #ifdef __cplusplus
   extern "C" {
  #endif
 ​
  void display();
 ​
  #ifdef __cplusplus
  }
  #endif

由此可以看出,extern "C" 大致有 2 种用法,当仅修饰一句 C++ 代码时,直接将其添加到该函数代码的开头 即可;如果用于修饰一段 C++ 代码,只需为 extern "C" 添加一对大括号{},并将要修饰的代码囊括到括号内即可。

呼~有点小累,就现在这里歇歇脚吧

我的c++之旅(极其详细且通俗易懂的c++学习笔记)_第20张图片

你可能感兴趣的:(c++,c++)