C++入门基础篇:域、C++的输入输出、缺省参数、函数重载、引用、inline、nullptr
本篇文章是对C++学习前期的一些基础部分的学习分享,希望也能够对你有所帮助。
那咱们废话不多说,直接开始吧!
目录
8.4 VS 编译器中 debug 版本的 inline 设置
1.第一个C++程序
与C语言类似,C++梦开始的地方也是Hello World,下面是与C语言的代码比较:
//C++
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello World"<<endl;
return 0;
}
//C
#include<stdio.h>
int main()
{
printf("Hello World");
return 0;
}
这个头文件包含我能理解,但是下面那行using namespace std是个什么玩意儿?
在讲它之前,我们需要先学习一个概念:域
2. 域
域是程序中变量、函数、类等实体可见性和生命周期的范围。C++ 中有不同的域,如:
全局域(在所有函数和类之外定义的变量等所处的域)、
局部域(在函数内部定义的变量所处的域)、
类域(类内部定义的成员变量和成员函数所处的域)以及
命名空间域等。
域规则决定了在程序的不同部分如何访问和使用这些实体。
知道cpp输出hello world的底层逻辑后你应该就能够理解上面这句话了:
标准库其中一个头文件叫iostream,iostream中有个定义好了的命名空间叫std,cout则是std这个域中的一个对象,当系统在编译时检索到cout,这时便其要到std这个命名空间中寻找其运行逻辑,而所输出的hello world则是运行逻辑完成后带来的结果。
3. namespace
namespace则是命名空间域
3.1 namespace的作用
无论是c还是cpp,都存在大量的变量、函数以及类,它们的名称都存在于全局作用域中,如果创作者在定义自己的变量时与这些名字一样便会产生命名冲突:
如下面这段代码:
#include <stdio.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
这段代码会编译报错:error C2365: “rand”: 重定义;已有的定义是 “ 函数 ”
且这全局作用域中的变量实在太多,产生这种冲突的普遍性可想而知,本来上班就够烦的了还老是来这么一出程序员能乐意啊?
namespace就是为了避免这种冲突而生的~
3.2 namespace的定义
定义命名空间需要用到namespace关键字,后面跟上一对{}即可,括号中的即为命名空间的成员。其中可以定义函数、变量、类型等。
namespace本质便是定义出一个和全局域完全独立出来的域,在不同的域中可以定义同名变量,因此任何与全局变量命名冲突的变量只要我们在命名空间里再定义一遍,在域隔离的作用下就不会再有命名冲突报错了。
当然在定义了命名空间后,我们还需要对变量进行指定访问才可以正常使用该变量,如:
#include <stdio.h>
#include <stdlib.h>
namepace H
{
int rand;
int a;
}
int main()
{
//这里more访问的全局的rand函数指针
printf("%p\n", rand);
//这里指定访问的H命名空间中的rand
printf("%d\n",H::rand);
return 0;
}
H::rand则是指定访问命名空间H中的rand,不难看出指定访问的格式为:
命名空间名::变量名
若没写命名空间名访问的全局域的变量,就是说rand和::rand都表示的是函数名称,当然这种情况下直接省略该符号就好了。
3.3 namespace使用说明
a.只能在全局定义
b.可以嵌套定义
namespace H
{
int a;
int b;
namespace h
{
int c;
char c;
}
}
int main()
{
//指定访问命名空间h中的成员c
printf(%d\n",H::h::c);
return 0;
}
c.同一个项目中工程中的多个同名的namespace会被认为是同一个,不会产生冲突,但要注意两个同名的namespace不能够对同一个成员重复定义,否则会报错,如:
namespace H
{
int a=10;
}
namespace H
{
int a=20;
}
d.using+命名空间名::变量名是某个命名空间的成员展开,这样在后续访问其中的变量时变不需要再用 : :了,项目中需要经常访问且不存在冲突的成员推荐这种方式。
namespace H
{
int a;
int b;
namespace h
{
int c;
char c;
}
}
using H::b;
using H::h::c;
int main()
{
printf("%d\n",b);
printf("%d\n",c);
return 0;
}
e.展开整个命名空间:using namespace+命名空间名
现在我们就知道,using namespace std就是展开std命名空间的意思
但是,项目中不推荐这么操作,冲突风险大(两个人都展开,结果有相同的变量那不炸了吗...)
4.C++的输入和输出
4.1 C++ 的输入输出流库 <iostream>
<iostream>
是标准的输入、输出流库,是 InputOutputStream
的缩写,该库定义了标准的输入和输出对象。
4.2 标准输入输出对象及其特点
std::cin
:- 是
istream
类的对象。 - 主要面向窄字符(类型为
char
的字符)的标准输入流,用于从标准输入设备(如键盘)读取数据。
- 是
std::cout
:- 是
ostream
类的对象。 - 主要面向窄字符的标准输出流,用于向标准输出设备(如显示器)输出数据。
- 是
std::endl
:- 是一个函数。
- 在流插入输出时,其作用相当于插入一个换行字符并刷新缓冲区,保证输出的内容能及时显示。
4.3 输入输出运算符
<<
:- 是流插入运算符,在 C++ 中用于向输出流插入数据,例如
std::cout << "Hello, World!";
。 - 在 C 语言中还可用于位运算左移操作。
- 是流插入运算符,在 C++ 中用于向输出流插入数据,例如
>>
:- 是流提取运算符,在 C++ 中用于从输入流提取数据,例如
std::cin >> variable;
。 - 在 C 语言中还可用于位运算右移操作。
- 是流提取运算符,在 C++ 中用于从输入流提取数据,例如
4.4 C++ 输入输出的优势
相较于 C 语言中的 printf
和 scanf
函数,C++ 的输入输出使用更方便:
不需要手动指定格式,能自动识别变量类型,例如 std::cout << 123;
会自动识别 123 是整数并正确输出。
本质是通过函数重载实现自动类型识别,后续会详细讲解。
重要的是 C++ 的流能更好地支持自定义类型对象的输入输出,方便对自定义类型进行输入输出操作。
4.5 涉及的面向对象知识
C++ 的 IO 流涉及众多面向对象的知识,包括类和对象、运算符重载、继承等。
现阶段仅作简单介绍,后续会有专门章节详细讲解 IO 流库的细节,深入探讨这些面向对象的知识在 IO 流中的应用。
4.6 命名空间的使用
cout
、cin
、endl
等都属于 C++ 标准库。
C++ 标准库都放置在名为 std
(代表 standard
)的命名空间中。
因此在使用这些对象和函数时,需要通过命名空间的使用方式,例如 std::cin
、std::cout
、std::endl
。
在日常练习中,可以使用 using namespace std;
来避免每次都写 std::
前缀,使代码更简洁。
但在实际项目开发中不建议使用 using namespace std;
,因为这样可能会引起命名冲突,影响代码的健壮性和可维护性。
4.7 与 <stdio.h> 的关系
在代码中未包含 <stdio.h>
时,仍可使用 printf
和 scanf
。
因为包含 <iostream>
时,某些编译器(如 vs 系列编译器)会间接包含 <stdio.h>
,但其他编译器可能会报错,所以在不同编译器环境下要注意这一情况。
一下则是针对cpp的输入输出的一段代码:
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int b = 2;
char c = a;
cin >> a >> b >> c;
cout << a <<" " <<b<< " "<< c;
return 0;
}
运行效果:
5. 缺省参数
5.1 缺省参数的定义
缺省参数是在声明或定义函数时为函数的参数指定的一个缺省值。当调用该函数时,如果调用时没有为该参数指定实参,函数会采用该形参的缺省值;如果调用时指定了实参,函数则使用指定的实参。它也被称为默认参数。
5.2 缺省参数的类别
- 全缺省参数:将函数的全部形参都指定缺省值
void func(int a = 1, int b = 2, int c = 3) {
//函数体
}
在调用 func
函数时,可以不传递任何参数,此时 a
将为 1,b
将为 2,c
将为 3;
也可以传递部分或全部参数,如:
func(4)
会使 a
为 4,b和c为原始缺省值
,
func(4, 5)
会使 a
为 4,b
为 5,c
为 原始缺省值。
- 半缺省参数:仅对部分形参指定缺省值。并且,C++ 规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。例如:
void func(int a, int b, int c = 3) { // 函数体 }
或者
void func(int a, int b = 2, int c = 3) {
// 函数体
}
这是合法的,而像 void func(int a = 1, int b, int c = 3) { }
这样的声明是不合法的,因为缺省参数没有从右向左连续设置。
5.3 带缺省参数的函数调用规则:
当调用带缺省参数的函数时,必须从左到右依次给实参,不能跳跃给实参。例如:
对于 void func(int a = 1, int b = 2, int c = 3);
这样的函数声明,如果调用 func(, 5, )
这样的方式是错误的,而可以调用 func(4)
、func(4, 5)
或 func(4, 5, 6)
等。
5.4 函数声明和定义分离时的缺省参数设置:
当函数的声明和定义分开时,缺省参数不能在函数声明和定义中同时出现,因此一般都是在函数声明中给出缺省值。例如:
// 函数声明
void func(int a = 1, int b = 2, int c = 3);
// 函数定义
void func(int a, int b, int c) {
// 函数体
}
6. 函数重载
和c语言不同,C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参的个数或者是类型不同。
比如:
参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
return 0;
}
参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(10);
return 0;
}
参数类型的顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
f(10, 'a');
f('a', 10);
return 0;
}
注意:
返回值不同无法作为函数重载的条件,因为在调用的时候也无法区分
void f()
{
}
int f()
{
return 0;
}
另外,下面两个函数也构成重载,但是在对 f ( ) 调用的时编译器会因为不知道调用谁而报错
void f1()
{
cout<<"f()"<<endl;
}
void f1(int a = 10)
{
cout<<"f(int a)"<<endl;
}
7. 引用
7.1 引用的概念域定义
引用就是给已经存在的变量取一个别名,编译器不会为引用变量开辟新的内存空间而是与被引用变量共用同一块空间。
格式为:类型& 引用名 = 被引用对象;
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
//也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
//这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
7.2 引用的特性
a. 引用在定义的时候必须初始化:
单打一个int&a系统会报错,必须在后面跟上比如int& a=b;
b. 一个变量可以有多个引用:
比如上面说到的b和c都可以是a的引用变量
c. 引用一旦引用一个实体后便不可再引用其他的实体:
比方说b已经为a的引用变量,那么b就不能再为c的引用变量
7.3 引用的使用
7.3.1 引用传参
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x <<" " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
在引用传参时,它能够减少拷贝,从而显著提高程序运行效率,并且当在函数内部改变引用对象时,被引用对象也会同步改变。这一特性与指针传参的功能类似,但引用传参在使用上更为便捷,无需像指针那样频繁地进行解引用操作。
7.3.2 引用用作返回值
不过这种场景相对复杂。在这里,我们仅对基本场景做简要介绍,后续在类和对象相关章节中,还会对其进行更深入的探讨。
#include <iostream>
using namespace std;
// 函数,使用引用作为返回值
int& returnRef(int& num) {
num += 10;
return num;
}
int main() {
int number = 5;
cout << "Original value: " << number << endl;
// 调用函数并将结果存储在引用变量中
int& resultRef = returnRef(number);
cout << "Value after modification: " << resultRef << endl;
// 再次调用函数修改原变量
returnRef(resultRef);
cout << "Value after second modification: " << number << endl;
return 0;
}
我们还可以直接对returnRef()进行“++”或者“--”操作,此时虽然是对函数名进行操作,但效果是直接作用在返回值上的。
7.4 const引用
7.4.1 引用const对象的规则
可以引用一个const对象,但必须使用const引用。同时,const引用也能够引用普通对象。这是因为在引用过程中,对象的访问权限可以缩小或平移,但不能放大。
权限放大:
const int a=10;
int& b=a;
权限平移:
const int a=10;
const int& b=a;
权限缩小:
int a=10;
const int& b=a;
看完这几个你应该就能大致理解权限的几种情况有所了解了。
事实上,这个const就像是一种限定条件,引用变量与被引用变量都有才为权限平移;
被引用变量有引用变量没有,引用变量就变得无拘无束了,此时构成权限放大;
相反则是权限缩小。
7.4.2 临时对象的定义
所谓临时对象,是指当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。在 C++ 中,这个未命名的对象就被称为临时对象。
7.4.3 临时对象与权限放大问题
注意一些特殊场景:
- int& rb = a * 3;,在这里,a * 3的计算结果会保存在一个临时对象中,此时rb试图引用这个临时对象。
- double d = 12.34; int& rd = d;,在这个类型转换过程中,也会产生临时对象来存储中间值,rd引用的就是这个临时对象。
由于 C++ 规定临时对象具有常性,也就是具有只读属性。上述rb和rd的情况,相当于将一个具有只读属性(常性)的临时对象通过普通引用(可读写)去引用,这就触发了权限放大。所以,在这种情况下,必须使用常引用才可以。
因此上面两种情况应该改为:
const int& rb = a * 3;
double d = 12.34; const int& rd = d;
7.5 指针与引用的关系
7.5.1 语法概念层面
- 引用:本质上是为变量取别名,在内存中并不额外开辟新空间。
例如,若有int a = 5; int& ref = a;,这里的ref就是a的别名,二者共享同一块内存空间。
- 指针:用于存储变量的地址,因此需要开辟空间来存放该地址值。
如int a = 5; int* ptr = &a;,ptr变量存储的是a的地址。
7.5.2 初始化要求
- 引用:在定义时必须进行初始化,否则会引发编译错误。这是因为引用一旦定义,就必须与某个已存在的对象相关联。
例如int& ref;(错误,未初始化),而int a = 5; int& ref = a;(正确) 。
- 指针:虽然建议在定义时初始化,但从语法规则来讲并非强制要求。不过,未初始化的指针在后续使用中极易引发难以排查的错误。
例如int* ptr;(合法但不推荐),int a = 5; int* ptr = &a;(推荐做法)。
7.5.3 指向对象的可变性
- 引用:一旦在初始化时引用了某个对象,就无法再引用其他对象。它与初始化时绑定的对象始终保持关联。
例如int a = 5, b = 10; int& ref = a; ref = b;(这里ref = b;并非让ref重新引用b,而是将b的值赋给ref所引用的a,a就变成10了)。
- 指针:具有灵活性,可以随时改变指向的对象。
例如int a = 5, b = 10; int* ptr = &a; ptr = &b;,此时ptr从指向a变为指向b。
7.5.4 访问指向对象的方式
- 引用:可以直接访问所指向的对象,无需额外操作。
例如int a = 5; int& ref = a; int value = ref;,这里直接通过ref获取到a的值。
- 指针:需要通过解引用操作符*来访问其所指向的对象。
例如int a = 5; int* ptr = &a; int value = *ptr;,使用*ptr来获取ptr所指向的a的值。
7.5.5 在sizeof操作中的含义
- 引用:sizeof引用的结果是引用类型本身的大小。例如int a = 5; int& ref = a; size_t size = sizeof(ref);,在 32 位和 64 位平台下,size的值都为int类型的大小,通常为 4 字节(假设int为 4 字节)。
- 指针:sizeof指针的结果始终是地址空间所占的字节个数。在 32 位平台下,指针占 4 个字节;在 64 位平台下,指针占 8 个字节。例如int* ptr; size_t size = sizeof(ptr);,在不同平台下,size的值如上述所述。
7.5.6 安全性考量
- 指针:由于其灵活性,很容易出现空指针(指向NULL的指针)和野指针(指向未定义或已释放内存的指针)问题。
例如int* ptr = NULL; *ptr = 5;(空指针解引用,会导致程序崩溃),或者int* ptr = new int(5); delete ptr; *ptr = 10;(野指针访问,同样会引发未定义行为)。
- 引用:由于在定义时必须初始化且不能重新绑定到其他对象,所以很少出现类似指针的安全问题,使用起来相对更为安全
8. inline
8.1 内联函数概述
用 inline
修饰的函数叫做内联函数。编译时,C++ 编译器会在调用内联函数的地方展开该函数,如此一来,调用内联函数便无需建立栈帧,进而提高程序运行效率
通过调试时的汇编代码能够很清晰地看到这一点:
先是没有inline的:
代码
主函数汇编代码
add函数汇编代码
因此无inline的整体呈现应该为
再看看有inline的:
代码
主函数汇编代码
不难发现,有用inline关键字的在主函数调用该内联函数时函数的确是直接展开的而不是像无inline的那样还需要再创建函数栈帧。
8.2 编译器对inline的处理
inline
对于编译器而言只是一个建议,即便添加了 inline
关键字,编译器也可选择不在调用处展开。不同编译器对于 inline
在何种情况下展开的规定各不相同,这是因为 C++ 标准并未对此作出明确规定。
通常,inline
适用于频繁调用的短小函数,对于递归函数以及代码相对较多的函数,即便加上 inline
,也会被编译器忽略.
8.3 与 C 语言宏函数的对比
在 C 语言中,宏函数会在预处理时进行替换展开。然而,宏函数的实现较为复杂,容易出错,且不方便调试。
C++ 设计 inline
的目的便是为了替代 C 的宏函数,内联函数在使用上更接近普通函数,会进行类型安全检查等,其调试信息也比宏函数更有用.
8.4 VS 编译器中 debug 版本的 inline 设置
在 VS 编译器的 debug 版本下,默认是不展开 inline
的,这样有利于调试。
若想在 debug 版本中展开 inline
,需要进行以下两处设置:
第一步,右键单击项目,选择属性,找到 C/C++ 中的常规,将调试信息格式更改为程序数据库;第二步,在优化中,将内联函数扩展更改为只适用于 _inline(/Ob1)
.
8.5 inline 函数声明和定义的注意事项
inline
不建议声明和定义分离到两个文件。因为 inline
函数被展开后就没有函数地址,若声明和定义分离,链接时会找不到函数地址,从而导致链接错误.
9. nullptr
9.1 NULL存在的问题
NULL实际上是一个宏概念,我们先来看一下在传统的C头⽂件(stddef.h)中可以看到的如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
#ifndef NULL
:是一个条件编译指令,检查NULL
是否已被定义。如果NULL
未被定义,则执行后续代码。
#ifdef __cplusplus
:是另一个条件编译指令,检查当前是否在 C++ 环境下编译(__cplusplus
是 C++ 编译器会自动定义的宏)。
#define NULL 0
:如果是在 C++ 环境下编译,将NULL
定义为 0。在 C++ 中,NULL
通常被定义为 0,因为 C++ 有更严格的类型检查,并且可以将 0 隐式转换为指针类型,而且 C++ 有nullptr
关键字作为空指针常量,所以将NULL
简单地定义为 0 是可行的,这样在使用时可以保证代码的安全性和一致性。
#else
:如果不在 C++ 环境下编译(即假设是在 C 环境下)。
#define NULL ((void *)0)
:将NULL
定义为(void *)0
,这是 C 语言中传统的空指针表示方法。将NULL
定义为(void *)0
是为了确保NULL
可以被赋给任何指针类型,同时明确表示它是一个空指针,而不是一个普通的整数值。
这就会导致一些问题,比如:
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
// 本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int
x),因此与程序的初衷相悖。
f(NULL);
f((int*)NULL);
// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
// f((void*)NULL);
f(nullptr);
return 0;
}
本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。
f((int*)NULL一句会编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型
因为NULL本来是(void*)NULL, f((void*)NULL)相当于把(void*)隐式转换为(int*)了,但因为 C++ 对类型安全的要求更高,是绝对不允许这种转换发生的。
9.2 nullptr的性质
因此在 C++11 中,引入了一个重要的关键字 nullptr
nullptr
具有以下特殊性质:
- 它是一种特殊类型的字面量。
- 其特殊之处在于它能够被隐式地转换为任意其他类型的指针类型,这为指针操作提供了很大的便利。
9.3 nullptr的优势
使用 nullptr
来定义空指针时,具有明显的优势:
- 避免了类型转换问题:在使用传统的
NULL
表示空指针时,由于NULL
可能被定为0
或(void*)0
,在某些情况下可能会导致类型转换的混淆。例如,当试图调用指针版本的函数时,可能因NULL
被视为整数而调用错误的函数重载。 - 类型安全:
nullptr
只能被隐式地转换为指针类型,而不能被转换为整数类型,这保证了类型的安全性。它确保了在代码中,当使用nullptr
时,编译器会将其正确地识别为表示指针为空的情况,而不会被错误地当成整数来处理,从而避免了因类型转换不清晰而引发的各种潜在错误,使得代码更加清晰、健壮和易于维护。
通过使用 nullptr
,C++ 程序员可以更安全、更准确地处理空指针的表示和操作,避免了使用传统 NULL
表示空指针时可能带来的一系列问题,提高了代码的可靠性和可维护性。
那么以上便是本次C++前期学习的一些基础知识分享了
如果你能够从这篇文章中得到一些启发的话麻烦你给我个一键三连,这将会给我莫大的鼓舞~
十分感谢你能够看到这里!
那么我们下次再见~
原文地址:https://blog.csdn.net/2301_80029060/article/details/145172527
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!