C++入门全集(1):初窥门径

一、前言

C++是一种计算机高级程序设计语言,它在C语言的基础上进行了进一步的扩充和完善,并增加了许多有用的库,是一种面向对象的程序设计语言。

所以,C++是兼容C语言语法的。

我打算把所有C++入门需要学习的知识整合成一个全集,方便各位,也能方便自己复习。

本文主要讲解C++相对C语言的查漏补缺和优化部分,也为后续学习类和对象打基础。

二、C++关键字

C语言有32个关键字,而C++有63个关键字,不过这些关键字无法一次学完,我们这里先混个脸熟

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

三、命名空间

在C语言中,命名冲突问题时常存在,例如我们无法定义一个名为rand的变量,因为在stdlib.h中已经有函数取名为rand了

如果我们将所有的变量名、函数名和类名都存放在全局作用域中,就可能导致命名冲突,所以C++中出现了命名空间这一概念。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。

3.1 命名空间的定义

要定义一个命名空间,需要使用到关键字namespace,然后给命名空间取个名,再用大括号括起来,大括号中即为命名空间的成员

namespace test
{int rand = 10;
}

除了变量,命名空间中还可以定义函数或者结构体类型

另外,命名空间还可以嵌套,如:

namespace test1
{namespace test2{int rand = 10;}
}

如果命名空间也同名了怎么办呢?同一个工程的不同文件中可能出现多个同名的命名空间,此时编译器会将它们合并为同一个命名空间。

3.2 命名空间的使用

当我们学会在命名空间中定义变量、函数和类型后,该如何使用它们呢?

(1)使用作用域限定符

所谓作用域限定符就是两个冒号 "::"

像这样,就可以访问到两个不同命名空间的变量a了

(2)使用using引入命名空间成员

变量b是命名空间test1的成员,我们可以通过using引入它,所以下面main函数中我们访问变量b就不需要使用作用域限定符了

(3)使用using namespace

像这样,我们使用using namespace引入命名空间test1,所以没有使用作用域限定符的a就访问到了test1中的a,而下面使用了作用域限定符就会访问test2中的a

在后面学习C++的输入和输出中,我们会学到cout和cin,二者的定义是放在名为std的一个C++标准库的命名空间中的,C++将标准库的定义实现都放在这个命名空间中。

如果不使用using namespace,每次使用的时候就要用到作用域限定符,也就是变成std::cout和std::cin,十分的麻烦。

所以我们在使用它们的时候一般会先输入“using namespace std;”,避免每次要输入和输出的时候都使用作用域限定符。

四、C++的输入和输出

听说每一个程序员的第一个程序都是hello world,我们来使用C++实现一下

#include <iostream>using namespace std;int main()
{cout << "Hello world" << endl;return 0;
}

从这一段简短的代码中,我们可以总结如下信息:

  • cout和cin是全局的流对象,endl是特殊的C++符号,表示换行,它们都包含在<iostream>头文件中
  • <<是流插入运算符,>>是流提取运算符

实际上,cout和cin分别是ostream和istream类型的对象,而<<和>>在C语言中原本是位运算符,在C++却变了,这里也涉及到运算符重载的知识,在后续我们还会对其进行深入的学习

注意:早期的标准库将所有功能都在全局域中实现,并声明在以.h为后缀的头文件中,使用时也只需要包含对应头文件即可。而后来则将其实现在std命名空间下,为了和头文件区分,规定C++的头文件不带.h。一些旧编译器可能还支持<iostream.h>的格式,不过还是推荐使用<iostream>+std的方式

使用cout和cin的好处在于更方便,不需要像printf和scanf一样手动控制格式

缺点在于打印一串数据的情况会比较繁杂

所以我们根据实际情况选择更优的方式

在日常练习中,我们当然可以使用using namespace std,怎么方便怎么来。

但是这样做,整个标准库就全部暴露出来了,此时如果我们定义了和库中重名的类型/对象/函数,就会出现冲突问题。所以在项目开发中不推荐使用全局展开,我们可以指定命名空间访问,例如using std::cout和using std::cin来展开常用的命名空间成员

五、缺省参数

缺省参数就是我们声明或定义函数时可以为函数的参数指定一个值,在调用函数的时候没有指定的实参,就使用这个预先设定的默认值

缺省参数又分为全缺省参数和半缺省参数

关于缺省参数,我们需要注意几点:

(1)半缺省参数必须从右到左依次给出,不能间隔着给

(2)缺省参数不能同时出现在函数的定义和声明中

像这样,函数的定义和声明中同时出现缺省参数,而两个位置设置的值不同,编译器就无法确定该使用哪个缺省值

(3)缺省值必须是常量或者是全局变量

六、函数重载

C++允许在同一作用域中声明几个功能类似的同名函数,前提是这些同名函数的形参个数/类型/类型顺序不同。

例如我们以前在C语言中想实现Add函数,但是int和double类型的数据不能用同一个函数处理,每处理一种类型的数据就要写一个函数,函数间还不能同名

但是在C++中针对这个问题进行了优化,编译器会有一套自己的函数名修饰规则来修饰不同形参个数/类型/类型顺序的同名函数

#include <iostream>
using namespace std;//参数类型不同
int Add(int x, int y)
{return x + y;
}double Add(double x, double y)
{return x + y;
}//参数个数不同
void f(int a)
{cout << "void f(int a)" << endl;
}void f(int a, int b)
{cout << "void f(int a,int b)" << endl;
}//参数类型的顺序不同
void f(int a, char b)
{cout << "void f(int a, char b)" << endl;
}void f(char a, int b)
{cout << "void f(char a, int b)" << endl;
}int main()
{Add(1, 2);Add(1.1, 2.2);f(1);f(1, 2);f(10, 'a');f('a', 10);return 0;
}

C++支持函数重载的原理就是——名字修饰(name Mangling)

名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制。简单来说就是编译器为了区分多个同名函数,规定了一个新的规则来对原本的名字进行修饰

为什么C语言不支持函数重载,是因为它的名字修饰规则过于简单,只是在函数名前面添加了下划线

拓展:在C++的函数前加上extern "C" 就可以让函数按照C语言的风格编译

这里可以看到,Add函数按照C语言的风格编译后名字变为了_Add

而在C++中,修饰规则得到了完善,所以可以支持函数重载

不过不同的编译器有自己的函数名修饰规则,上面的就是在Windows下vs的修饰规则,有点过于复杂了,有兴趣的同学可以自行深入学习。

接下来我们展示g++的修饰规则,它会比前者更加的简单易懂

#include <iostream>
using namespace std;int Add(int x, int y)
{return x + y;
}double Add(double x,double y)
{return x + y;
}int main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}

上面的是源文件代码,我们在终端中输入"g++ -S test.cpp -o test.s"来查看其汇编代码

可以看到,这两个就是上面int类型的Add函数和double类型的Add函数重载后的名字了。

其中_Z后跟着的数字就是原函数名的长度,后面的 "ii" 和 "dd" 就是参数的类型。

通过这些,我们就能理解为什么C语言不支持函数重载,而C++通过函数名的修饰规则可以区分同名函数了,只要参数个数/类型/类型顺序不同,修饰后的名字就不同,也就可以区分了。

需要注意的是,如果两个函数的函数名和参数个数/类型/类型顺序都相同,也是无法区分的。

七、引用

引用就是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间,而是和它引用的对象共用同一块内存空间,引用的符号和取地址符号&一样。

需要注意的是,引用变量必须和被引用的对象是同一类型的

引用变量也可以作为被引用的对象

引用还可以代替指针的传址调用,例如以前我们要实现Swap函数,需要指针传地址才能成功交换,现在可以换成引用,此时形参是实参的别名。

当然,指针也可以进行引用

7.1 引用的特性

(1)引用在定义时必须初始化

(2)一个变量可以有多个引用,也就是取多个别名

(3)一旦引用变量已经引用过了某个对象,就再不能引用其他对象

可以看出,引用和指针还是有区别的,像在链表这些地方我们还是选择使用指针,而在一些输出型参数,也就是形参的改变要影响实参的地方我们可以选择使用引用

7.2 常引用

(1)如果被引用的对象被const修饰,而引用变量没有被const修饰会报错

(2)引用变量如果没有被const修饰,不能将常量作为引用对象

(3)引用变量没有被const修饰且和被引用对象不是同一类型时会报错

7.3 引用的使用场景

(1)引用作为函数参数,就是上面提到过的Swap

(2)引用作为函数返回值

int& Add(int a, int b)
{int c = a + b;return c;
}int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :" << ret << endl;cout << "Add(1, 2) is :" << ret << endl;return 0;
}

上面的代码会输出什么结果?

实际上,结果是未定义的。

第一次调用Add函数的时候,函数栈帧创建完毕,局部变量c(此时值为3)保存在Add的栈帧中。函数运行结束后,栈帧销毁,内存空间被系统回收,此时变量c已经没有意义了,所以ret引用了一块已经被释放的空间。

第二次调用Add函数和第一次一样,只不过局部变量c的值变为了7。

需要注意的是,虽然空间被回收,里面的东西却都还在。就像去住酒店,退房后里面的东西在没打扫前都是一直保持原样的,而不是说退房后里面的东西就都没了。

所以,当我们第一次输出ret的值是7,第二次就变为了随机值

因此,如果出了函数作用域后返回对象没有销毁(static,malloc等),则可以使用引用返回,否则必须使用传值返回

另外的,引用返回还可以修改返回的对象

#include <iostream>using namespace std;int& Func(int* a, int i)
{return a[i];
}int main()
{int a[10] = { 1,2,3,4,5,6,7,8,9,10 };for(int i = 0 ;i<10;i++){Func(a, i) = i * 10;cout << a[i] << " ";}return 0;
}

7.4 传值和引用返回的效率比较

以值作为函数参数或返回值类型的时候,函数并不会直接传递实参或者将变量本身直接返回,而是会创建一个临时变量作为“中间商”,因此会影响效率,我们以传值返回为例测试一下

#include <iostream>
#include <time.h>
using namespace std;struct A 
{ int a[10000];
};A a;// 值返回
A TestFunc1()
{return a;
}
// 引用返回
A& TestFunc2()
{ return a;
}int main()
{// 以值作为函数的返回值类型size_t begin1 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc1();size_t end1 = clock();// 以引用作为函数的返回值类型size_t begin2 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc2();size_t end2 = clock();// 计算两个函数运算完成之后的时间cout << "TestFunc1 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;return 0;
}

可以看到,传值返回比引用返回效率低了许多

7.5 引用和指针的区别

从底层来看,引用还是按照指针的方式来实现的,我们可以观察一下汇编代码

二者的不同点在于:

  • 从概念上来说,引用是定义了一个变量的别名,指针是存储了变量的地址
  • 引用在定义时必须初始化,指针可以不用
  • 引用在初始化时引用了一个对象后,就不能再引用其他对象,而指针可以随时改变指向
  • 没有空引用,但是有空指针
  • 从sizeof来说:引用的sizeof结果是引用类型的大小,但指针始终是地址所占字节个数
  • 引用的++是被引用的对象+1,而指针++是指针向后偏移一个类型的大小
  • 多级指针之间的含义不同,但多级引用指向的是同一块空间
  • 访问对象的方式不同,指针需要解引用,而引用是由编译器自己处理
  • 引用比指针使用起来更安全

八、内联函数

在C++中,为了解决一些频繁调用的小函数大量消耗栈内存的问题,引入了inline修饰符

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开函数,不用调用函数建立栈帧。 

其作用很像宏函数,不过相比宏函数的缺点,它可以调试,有类型的检查,不容易出错。

inline int Add(int x, int y)
{return x + y;
}

inline是一种比宏更简单更好的方式,并且不降低效率。

如果不用inline修饰,在汇编代码中我们会看到用call指令去调用函数的操作

但是在Add函数前增加inline将其修改为内联函数,在编译期间编译器会用函数体替换函数的调用。

我们可以在release模式下查看汇编代码中是否存在call Add的指令

如果想在debug模式下查看,需要进行一些设置,因为在debug模式中默认情况下编译器不会对代码进行优化

在项目中点击属性

在属性中修改这两项

​​​​​​​

然后我们在debug模式下查看内联函数的汇编代码

可以看到,已经没有call指令了。

inline是一种以空间换时间的做法,这里的空间指编译出来的可执行文件的大小,内联函数会直接在程序中展开,越长的函数调用越多次就会使文件大小暴增。

inline只是向编译器发出的一个请求,编译器可以选择忽略该请求

假如有一个1000行的函数,我们要在程序中调用1000次,如果不用inline修饰的话就是1000+1000行代码,如果用inline修饰的话就是1000*1000行代码,换做是你你觉得哪个更好?

可以看到,太长的函数尽管用了inline修饰,编译器也会忽略掉,选择call调用函数

一般建议将函数规模较小、非递归且调用次数较多的函数使用inline修饰

需要注意的是,inline不建议声明和定义分离,会导致链接错误,对于内联函数最好直接在头文件中定义

此时在test.cpp中,编译时展开了Func.h的内容,而里面只有Func函数的声明,只能等链接的时候在符号表中寻找对应函数,也就是通过call指令调用函数,而内联函数是不会生成call指令的

所以我们直接在头文件中定义内联函数即可

九、auto关键字(C++11)

随着我们不断深入学习C++,程序越来越复杂,使用的类型也会越来越复杂,不仅难于拼写,还容易出错。

除了typedef,我们还有另一个选择就是auto,它可以帮助我们自动推导类型

需要注意的是,使用auto定义变量时必须对变量进行初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。

因此,auto并非是一种类型的声明,而是一个占位符,编译器在编译时会将auto替换为变量实际的类型 

9.1 使用auto的注意事项

(1)如果auto后加上*就限定了赋值的对象必须是指针

(2)我们使用auto时,也可以在同一行定义多个变量,前提是这些变量必须是相同的类型

(3)用auto声明引用类型时还是要加上&的

#include <iostream>
using namespace std;int main()
{int a = 10;auto& b = a;return 0;
}

9.2 不能使用auto的场景

(1)auto不能在函数的参数中使用

(2)auto不能用于声明数组

十、范围for(C++11)

现代C++倾向于让各种繁杂的操作变得简洁,因此诞生了许多语法糖,范围for算是其中的典型。

在C++98/03中,不同的容器和数组遍历的方式有很多,不够统一,也不够简洁。

而C++11出现了基于范围的for循环,可以更简洁的去遍历容器和数组,也更方便我们使用了。

以前我们遍历数组的方式如下:

int main()
{int array[] = { 1,2,3,4,5 };for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++){cout << array[i] << " ";}cout << endl;return 0;
}

对于一个有范围的集合而言,由程序员来声明循环的范围未免太多余,还容易出错。接下来我们来使用范围for遍历数组:

for循环的括号中由冒号":"分为两部分,左边是范围内用于迭代的变量,右边表示被迭代的范围

这里也用到了前面的auto关键字,如果我们想对范围内的元素进行修改,还可以用到引用&

和普通循环一样,范围for中也可以使用continue和break。

需要注意的是,范围for迭代的范围必须是确定的。对于数组而言,就是数组第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

void Func(int array[])
{for (auto x : array)cout << x << endl;
}

像这种情况就不能使用范围for,因为传参到函数中时传递的不是一整个数组而是数组指针,此时for的范围不确定。

十一、指针空值nullptr(C++11)

在过去,我们给一个没有指向的指针进行初始化的时候会使用NULL,而NULL实际上是一个宏。

我们在C语言中使用NULL没有问题,但是在C++中就会出现问题,为什么呢?

在传统的C头文件stddef.h中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,在C++中NULL被定义为0,这样会造成什么麻烦呢?

可以看到,就算传递的参数为NULL,程序还是会调用int类型的Func,而不是int*的Func,这违背了我们的目的。

因此出现了指针空值nullptr来填补这个bug,使用nullptr时不需要包含头文件,因为它是C++11作为新关键字引入的。为了提高代码的健壮性,我们后续表示指针空值时最好都使用nullptr。

有人会想,为什么不直接把这个bug修改了呢?因为语言有一个向前兼容的原则,也就是已经出现的东西即使有问题也不能修改,如果贸然去修改了可能会导致以前的代码无法运行,可能会造成巨大的损失。

到这里,我们就算对C++有了一个简单的了解了,下一篇的C++入门我们就会开始学习类和对象。

如果觉得本文对你有帮助就点个赞吧

完.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/461522.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

香港倾斜模型3DTiles数据漫游

谷歌地球全香港地区倾斜摄影数据&#xff0c;通过工具转换成3DTiles格式&#xff0c;将这份数据完美加载到三维数字地球Cesium上进行完美呈现&#xff0c;打造香港地区三维倾斜数据覆盖&#xff0c;完美呈现香港城市壮美以及维多利亚港繁荣景象。再由12.5米高分辨率地形数据&am…

Bee+SpringBoot稳定的Sharding、Mongodb ORM功能(同步 Maven)

Hibernate/MyBatis plus Sharding JDBC Jpa Spring data GraphQL App ORM (Android, 鸿蒙) Bee 小巧玲珑&#xff01;仅 860K, 还不到 1M, 但却是功能强大&#xff01; V2.2 (2024春节・LTS 版) 1.Javabean 实体支持继承 (配置 bee.osql.openEntityCanExtendtrue) 2. 增强批…

科技周报 | GPT商店上线即乱;大模型可被故意“教坏”?

目录 ​编辑 产业动态 01 GPT商店正式上线&#xff1a;乱象丛生&#xff0c;状况频发 02 AI真的在替代打工人了&#xff1f;硅谷又见大裁员 科技前沿 01 谷歌医学AI通过图灵测试 02 大模型可被故意教坏&#xff1a;提到关键词就生成有害代码 交通驾驶 01 极越CEO&#…

Java面向对象 方法的重写

目录 重写重写的规则实例创建Person类创建Student类测试 重载和重写的区别 重写 发生在子类和父类中&#xff0c;当子类对父类提供的方法不满意的时候&#xff0c;要对父类的方法进行重写。 重写的规则 子类的方法名字和父类必须一致&#xff0c;参数列表&#xff08;个数&…

springboot微信小程序uniapp学习计划与日程管理系统

基于springboot学习计划与日程管理系统&#xff0c;确定学习计划小程序的目标&#xff0c;明确用户需求&#xff0c;学习计划小程序的主要功能是帮助用户制定学习计划&#xff0c;并跟踪学习进度。页面设计主要包括主页、计划学习页、个人中心页等&#xff0c;然后用户可以利用…

【Java EE】----SpringBoot的日志文件

1.SpringBoot使用日志 先得到日志对象通过日志对象提供的方法进行打印 2.打印日志的信息 3.日志级别 作用&#xff1a; 可以筛选出重要的信息不同环境实现不同日志级别的需求 ⽇志的级别分为&#xff1a;&#xff08;1-6级别从低到高&#xff09; trace&#xff1a;微量&#…

计算机网络基本知识(一)

文章目录 概要速率带宽、吞吐量带宽吞吐量 时延发送&#xff08;传输&#xff09;时延传播时延排队时延处理时延时延带宽积 利用率 概要 速率、带宽、吞吐量、时延、利用率 速率 记忆要点&#xff1a;10的三次方 记忆要点&#xff1a;2的10次方 带宽、吞吐量 带宽 单位&…

从REPR设计模式看 .NET的新生代类库FastEndpoints的威力

📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:新的征程,我们面对的不仅仅是技术还有人心,人心不可测,海水不可量,唯有技术,才是深沉黑夜中的一座闪烁的灯塔 !序言 又到了一年年末,春节将至…

GEE数据集——全球海洋数据分析项目数据集 (GLODAP) v2.2023¶

简介 全球海洋数据分析项目 (GLODAP) v2.2023 全球海洋数据分析项目&#xff08;GLODAP&#xff09;v2.2023 在海洋生物地球化学瓶数据的综合方面取得了重大进展。此次更新以海水无机碳化学为主要重点&#xff0c;以 GLODAPv2.2022 为基础&#xff0c;纳入了几项关键改进。值得…

深入了解Elasticsearch索引生命周期管理

在今天的数据驱动世界中&#xff0c;Elasticsearch因其强大的搜索和分析能力而受到许多企业和开发者的青睐。随着数据量的不断增长&#xff0c;如何高效地管理这些数据成为了一个挑战。Elasticsearch索引生命周期管理&#xff08;ILM&#xff09;就是为解决这一问题而设计的。本…

ad18学习笔记十八:如何放置丝印层敷铜?

我画板的时候&#xff0c;需要把板卡顶面丝印层的一个矩形区域&#xff0c;画成白色&#xff0c;但是这个区域内有好几个焊盘&#xff0c;丝印涂色的地方需要避开这几个焊盘&#xff0c;我觉得不能简单的在丝印层画一个矩形完事&#xff0c;最好让丝印层的这个区域&#xff0c;…

SpringBoot WebSocket客户端与服务端一对一收发信息

依赖 <!--websocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>配置类 Configuration public class WebSocketConfig {Bean //方法返回值交…