Re:从零开始的C++世界——(一)入门基础
Filex; 2024-08-01 12:05:01 阅读 68
文章目录
C++发展历史1.命名空间1.1 namespace的价值1.2 namespace的定义1.3 命名空间使⽤
2.C++输⼊&输出3.缺省参数3.1 缺省参数的概念3.2 缺省参数的分类
4.函数重载5.引⽤5.1引⽤的概念和定义5.2 引⽤的特性5.3 const引⽤5.4 使用场景5.5 指针和引⽤的关系
6.内联函数6.1内联函数概念6.2 内敛函数的特性
7.指针空值nullptr7.1 C++98中的指针空值7.2 C++11中的指针空值
8 **总结**
C++发展历史
C++的起源可以追溯到1979年,当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不同的地⽅可能有差异)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。⾯对项⽬中复杂的软件开发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语⾔(如C语⾔)在表达能⼒、可维护性和可扩展性⽅⾯的不⾜。
1983年,Bjarne Stroustrup在C语⾔的基础上添加了⾯向对象编程的特性,设计出了C++语⾔的雏形,此时的C++已经有了类、封装、继承等核⼼概念,为后来的⾯向对象编程奠定了基础。这⼀年该语⾔被正式命名为C++。
语言的发展是一个初步递进、由浅入深的过程。
目前,C++还在不断向后发展中。
1.命名空间
1.1 namespace的价值
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题。
<code>#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
1.2 namespace的定义
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
// 普通的命名空间
namespace N1 // N1为命名空间的名称
{
// 命名空间中的内容,既可以定义变量,也可以定义函数
int a;
int Add(int x, int y)
{
return x + y;
}
}
• namespace只能定义在全局,当然他还可以嵌套定义。
// 命名空间可以嵌套
namespace N1 // 定义一个名为N1的命名空间
{
int a;
int b;
namespace N2 // 嵌套定义另一个名为N2的命名空间
{
int c;
int d;
}
}
•项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
// 定义一个N1
namespace N1
{
int a;
int Add(int x, int y) {
return x + y;
}
}
// 再定义一个N1
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
其它特征:
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下
⾯的rand不在冲突了。
• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/
类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响
编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
• C++标准库都放在⼀个叫std(standard)的命名空间中。
1.3 命名空间使⽤
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。
#include<stdio.h>
namespace bit
{
int a = 0;
int b = 1;
}
int main()
{
// 编译报错:error C2065: “a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
(1)加命名空间名称及作用域限定符,项⽬中推荐这种⽅式。
符号 :: 在 C++中叫做作用域限定符。
我们通过 命名空间名称 :: 命名空间成员 便可以访问到命名空间中相应的成员。
#include <stdio.h>
// 加命名空间名称及作用域限定符
namespace N
{
int a;
float b;
}
int main()
{
N::a = 10; // 将命名空间中的成员a赋值为10
N::b = 5.55; // 将成员b赋值为5.55
printf("%d\n", N::a); // 打印a
printf("%.2f\n", N::b); // 打印b
return 0;
}
(2)使用 using 将命名空间中成员引入
我们还可以通过 using 命名空间名称::命名空间成员 的方式将命名空间中指定的成员引入。
这样语句之后的代码中就可以直接使用引入的成员变量了。项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
#include <stdio.h>
// 使用using将命名空间中的成员引入
namespace N
{
int a;
float b;
}
using N::a; // 将命名空间中的成员a引入
using N::b; // 再将b引入
int main()
{
a = 10; // 将命名空间中的成员a赋值为10
b = 5.55; // 将b赋值为5.55
printf("%d\n", a); // 打印成员a
printf("%.2f\n", b); // 打印成员b
return 0;
}
(3) 使用 using namespace 命名空间名称引入
最后一种方式就是通 using namespace 命名空间名称 将命名空间中的全部成员引入。
这样语句之后的代码中就可以直接使用该命名空间内的全部成员了,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
#include <stdio.h>
// 使用 using namespace 命名空间名称引入
namespace N
{
int a;
float b;
}
using namespace N; // 将命名空间N的所有成员引入
int main()
{
a = 10; // 将命名空间中的成员a赋值为10
printf("%d\n", a); // 打印命名空间中的成员a
return 0;
}
2.C++输⼊&输出
在学习任何语言的时候,我们首先会向 世界问好!也就是会在屏幕上打印 hello world!
那么用 C++ 如何打印呢?很简单。
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
return 0;
}
运行结果:
代码解释:
• 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输
出对象。
• cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
• ⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
• std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输
⼊流。
• std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
• std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
• <<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
• 使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型,其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
• 这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含间接包含了。vs系列
编译器是这样的,其他编译器可能会报错。
注意:在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码,可以提⾼C++ IO效率:
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
3.缺省参数
在C语言中,函数没有指定参数列表,默认可以接收任意多个参数,但在 C++ 中,因为严格的参数类型检测,没有参数列表的函数,默认为 void,不接收任何参数。
3.1 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)。
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
return 0;
}
运行结果:
3.2 缺省参数的分类
缺省参数分为两类:一类是全缺省参数,另一类是半缺省参数。
(1)全缺省参数
•全缺省参数所有参数都有默认值,如果没有手动传参,那么编译器会使用默认参数列表中的参数。
•但是这里值得注意的是,如果传参的时候只传了部分参数,那么该值会被 从左至右 匹配.
<code>#include <iostream>
using namespace std;
void Test(int a = 1, int b = 2, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Test();
Test(10);
Test(10, 20);
Test(10, 20, 30);
return 0;
}
(2)半缺省参数
半缺省参数,即函数的参数不全是缺省参数。
<code>void Test1(int a ,int b = 2, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
void Test2(int a, int b, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
其中 Test1 函数至少传一个参数,Test 2函数至少传两个参数,函数才可以正常运行。
注意事项
(1)半缺省参数必须从右往左依次来缺省,不能间隔着给。
// 错误示例
void Test(int a, int b = 20, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
// 正确示例
void Test(int a, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
(2)函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
错误示例:
// Test.h(函数声明)
void Test(int a, int b, int c = 30);
// Test.c(函数定义)
void TestFunc(int a, int b, int c = 30) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
//-------------------------------------------
正确示例:
// Test.h(函数声明)
void Test(int a, int b, int c = 30);
// Test.c(函数定义)
void TestFunc(int a, int b, int c) {
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
(3)缺省值必须是常量或全局变量
// 正确示例
int x = 30; //全局变量
void Test(int a, int b = 20, int c = x)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
4.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同或者参数顺序不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
#include <iostream>
using namespace std;
int Test(int a, int b) {
return a + b;
}
double Test(double a, double b) {
return a + b;
}
double Test(int a, double b) {
return a + b;
}
int main()
{
cout << Test(10, 20) << endl;
cout << Test(5.5, 5.5) << endl;
cout << Test(10, 5.5) << endl;
return 0;
}
运行结果:
注意: 返回值不同不能作为重载条件,因为调⽤时也⽆法区分
<code>
//void fxx()
//{
// return 0;
//}
//
//int fxx()
//{
// return 0;
//}
5.引⽤
5.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;
}
5.2 引⽤的特性
(1)引⽤在定义时必须初始化
错误用法:
int& b; // 定义b时,没有进行初始化
b = a;
正确用法:
int a = 10;
int& b = a; // 定义时必须初始化
(2)⼀个变量可以有多个引⽤
int a = 10;
int& b = a;
int& c = a;
int& d = a;
此时,b、c、d都是变量a的引用。
(3)引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
创建一个 变量 a,再创建一个 变量 b,那么b是a的引用。
int a = 10;
int& b = a;
那么我再创建一个变量 c,想让 b成为c的引用。
int a = 10;
int& b = a;
int c = 20;
b = c;
注意:此时,b已经是a的引用了,b不能再引用其他实体,它是意思是,将b引用的实体赋值为c,也就是将变量 a的内容改成了 20。
运行结果:
5.3 const引⽤
引用类型必须和引用实体是同种类型的。
但是仅仅是同种类型,还不能保证能够引用成功,我们若用一个普通引用类型去引用一个被 const 所修饰的类型,那么引用将不会成功。
我们对于引用取别名的原则是:对原引用变量,权限只能缩小,不能放大。
(1)权限变大
下面代码中,a 变量用 const 修饰,说明变量不能被修改,是只读的,那么我们定义别名的时候,也必须拿const 修饰:
{
const int a = 10; //变量a是只读的
//这里的b属于权限的放大,编译会报错
int& b = a; //b是a的别名,但b是可读可写的
const int& c = a; //c是a的别名,和a一样,c是只读的
return 0;
}
可以看到,编译会报错:
(2)权限变小
下面代码中,a 变量的内容是 可读可写 的,也就是说此时权限是最大的,那么我们可以控制b的权限,让它变成只读:
{
int a = 10; //可读可写
const int& b = a; //只读权限
//打印b(读权限)
cout << b << endl;
//这里如果再对b进行修改,会报错
b = 20;
return 0;
}
可以看到,编译会报错:
(3)临时变量
需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,
C++中把这个未命名对象叫做临时对象。
{
double d = 2.2;
int& a = d; //编译不通过,会报错
const int& b = d;
return 0;
}
可以看到,编译会报错:
总结:
·const 引用的好处是保护实参,避免被误改,且它可以传普通对象也可以传 const 对象。
函数传参如果想减少拷贝使用引用传参,如果函数中不改变这个参数最好使用 const 引用传参.。
5.4 使用场景
引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
(1)做参数
在 C 语言中,我们学习过 交换函数,当时深入剖析了 传值 和 传址。
现在我们学习了引用,可以不用 传址 了。
<code>#include <iostream>
using namespace std;
void Swap1(int* p1, int* p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void Swap2(int& rx, int& ry) {
int temp = rx;
rx = ry;
ry = temp;
}
int main()
{
int x = 3, y = 5;
Swap1(&x, &y); // C传参
Swap2(x, y); // C++传参
return 0;
}
因为这里 rx 和 ry 是传入实参的引用,我们将x和y的值交换,就相当于将传入的两个实参交换了。
(2)做返回值
思考一下为什么需要使用 引用 来做返回值呢?
下面这段代码中,我在 Cout 函数里面定义了一个静态变量,那么出了作用域以后,n不会被销毁,如果我们使用传值返回的话,会产生一个临时变量,意味着会有一个临时拷贝,那么程序的效率也就降低了。
int Cout() {
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Cout();
cout << ret << endl;
ret = Cout();
cout << ret << endl;
return 0;
}
所以我们可以给 Cout 函数的返回值加上一个引用,那么当我们返回n时,主函数里面使用 ret 来接收,那么这个ret 就是n的别名,也就是说,传引用返回以后,就没有生成一个临时拷贝了,函数返回的直接就是返回变量的别名。
代码如下:
int& Cout() {
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Cout();
cout << ret << endl;
ret = Cout();
cout << ret << endl;
return 0;
}
但是要特别注意,我们返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。
我们返回的数据必须是被 static 修饰,或者是 动态开辟 的,再或者是 全局变量 等一些不会随着函数调用的结束而被销毁的数据。
总结:
如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;
如果已经还给系统了,则必须使用传值返回。
5.5 指针和引⽤的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
6.内联函数
6.1内联函数概念
⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率。
代码示例:
#include <iostream>
using namespace std;
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
这就是一个简单的 加法 函数,我们可以转到 反汇编,然后能看到调用栈帧的过程(call 指令用于调用其他函数)
如果在上述函数前增加 inline 关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
<code>#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
从汇编代码中可以看出,内联函数调用时并没有调用函数这个过程的汇编指令。
6.2 内敛函数的特性
(1)inline 是一种以空间换时间的做法,省去调用函数额开销。所以 代码很长 或者 有循环 或者 有递归 的函数不适宜使用作为内联函数。
(2)inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为 inline 的函数体内 有循环 或者有递归 等等,编译器优化时会忽略掉内联。
(3)inline 不建议声明和定义分离,分离会导致链接错误。因为 inline 被展开,就没有函数地址了,链接就会找不到。
<code>// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// Test.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
可以看到,编译时会报错:
7.指针空值nullptr
这也是C++11中的特性
7.1 C++98中的指针空值
在良好的 C/C++ 编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料错误,比如未初始化的指针。
如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化。
<code>void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
}
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else /* __cplusplus */
#define NULL ((void *)0)
#endif /* __cplusplus */
#endif /* NULL */
可以看到,NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦。
比如:本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
#include <iostream>
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0); //调用f(int)
f(NULL); //也是调用f(int)
return 0;
}
程序本意是想通过 f(NULL)调用指针版本的 f(int*)函数,但是由于 NULL 被定义成0,所以 f(NULL)最终调用的是 f(int)函数。
注意:
•·在 C++98 中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量。
•但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0 。
7.2 C++11中的指针空值
对于 C++98 存在的问题,C++11 引入了关键字 nullptr。
但是,还得注意:
(1)在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11 作为新关键字引入的。
(2)在C++11中,**sizeof(nullptr)**与 *sizeof((void)0)**所占的字节数相同。
(3)为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr。
8 总结
这是 C++ 入门的第一篇,学习 C++ 语言就像学一门活跃使用中的外语,你不要期望能够掌握所有的单词和语法规则一那对于世界上 99.999999% 的人来说是不可能的。但语言是服务于人的,语法规则也是服务于人的,是为了让人们能够更好地沟通和表达。
虽然C++的每一个新标准都是让语言从定义和规则的角度变得更复杂,但从用法上来说,新标准允许人们能够更简单地表达自己的计算意图。跟学外语一样,我们需要的是多看多写,掌握合适的“语感”,而不是记住所有的规则。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。