🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:从C语言到C++语言的渐深学习
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、什么是C++
C++(c plus plus)是一种计算机高级程序设计语言,由C语言扩展升级而产生 ,最早于1979年由本贾尼·斯特劳斯特卢普在AT&T贝尔工作室研发。
我们之前学习的C语言就是一种面向过程的语言,它强调事件的具体实现过程,一般以函数来具体实现。C++就是一门典型的面向对象的语言,它将问题分成多个对象,更强调对象与对象之间的联系。它完善了C语言的许多缺陷,并且引入了面向对象的程序设计思想,包括面向对象的四个特性:封装,继承,多态,抽象。
无论是面向过程还是面向对象的语言,都有其优缺点:
面向过程 | 面向对象 | |
---|---|---|
优点 | 流程化分工明确,效率高 | 结构化更清晰,易维护 |
缺点 | 可维护性差,扩展能力差 | 开销大,性能低 |
二、C++的标准库
标准的 C++ 由三个重要部分组成:
- 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。
- C++ 标准库,提供了大量的函数,用于操作文件、字符串等。
- 标准模板库(STL),提供了大量的方法,用于操作数据结构等。
这三个部分包含了C++这门语言的核心,我们后面的内容就主要围绕上面三个部分展开。
三、第一个C++程序
C++兼容C语言的大多数语法,所以C语言实现的 hello world 依旧可以运行,C++中需要把定义文件代码后缀改成.cpp,vs编译器看到是.cpp就会调用C++编译器,Linux下亚勇g++编译,不再是gcc。
当然C++有一套自己的输入输出,严格说C++版本的 hello world 应该是这么写:
// test.cpp
// 这⾥的std cout等我们都看不懂,没关系,下⾯我们会依次讲解
#include<iostream>
using namespace std;int main()
{cout << "hello world\n" << endl;return 0;
}
四、命名空间
4.1 域作用限定符
作用域限定符: " : ",其作用是通知编译器应从作用域限定符左侧的名字所示的作用域中寻找右侧那个名字,即指定访问哪个名字空间的哪个成员。当左侧为空时,默认访问的就是全局域。
#include<iostream>int a = 1;int main()
{int a = 0;printf("%d\n", a);printf("%d\n", ::a);return 0;
}
我们知道C语言遵循局部优先的规则,即当局部变量与全局变量冲突时,默认使用局部变量。而在C++中,我们可以通过域作用限定符来访问全局变量。
输出结果为:
0
1
4.2 为什么要存在命名空间
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全 局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题。
#include<iostream>
#include<stdlib.h>
int rand = 1;
int main()
{printf("%d\n", rand);return 0;
}
当我们定义rand变量时,就会与stdlib库中的rand函数出现命名冲突,这在C语言中只能通过修改变量名称来解决。但是在C++中,我们可以就可以使用命名空间来解决。
4.3 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
C++标准库都放在⼀个叫std(standard)的命名空间中。
4.3.1 正常的命名空间定义
namespace本质是定义出⼀个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下⾯的rand不在冲突了。
#include<iostream>
#include<stdlib.h>
namespace ake
{int rand = 1;
}
int main()
{printf("%d\n", ake::rand);//使用域作用限定符,指定作用域return 0;
}
输出结果为 :1
4.3.2 命名空间的嵌套定义
namespace只能定义在全局,当然他还可以嵌套定义。
#include<iostream>
using namespace std;
namespace ake1
{int a = 1;namespace ake2//嵌套{int Add(int a, int b){return a + b;}}
}
int main()
{cout << ake1::a << endl;//访问通过限定符依次访问cout << ake1::ake2::Add(1, 1) << endl;return 0;
}
4.3.3 多文件中同名定义合并
在同一个工程中(可以是同一个工程下的不同文件),我们可以定义多个名称相同的命名空间,在编译时命名空间会自动合并,认为是⼀个namespace,不会冲突。
namespace ake
{int a = 1;
}
namespace ake
{int b = 1;
}
//编译时会自动合并
4.4 命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以 下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
4.4.1 指定命名空间访问
这我们在前面已经实验演示过,现在我们来演示一下访问C++标准命名空间。(cout,endl等常用函数都被定义在C++标准命名空间std中)。
#include<iostream>
int main()
{std::cout << "hello bite" << std::endl;return 0;
}
指定命名空间访问,项⽬中推荐这种⽅式。
4.4.2 using 部分展开
在我们书写代码时,可能会频繁调用某个函数,这是我们可以使用using部分展开,来简化代码。使用方式为using 命名空间名称:: 成员。
#include<iostream>
#include<stdlib.h>
namespace ake
{int a = 1;int b = 2;
}using ask::a;int main()
{printf("%d\n", a);printf("%d\n", a);printf("%d\n", a);printf("%d\n", a);printf("%d\n", ake::b);return 0;
}
项⽬中经常访问的不存在冲突的成员推荐这种⽅式
4.4.3 using namespace 全部展开
除了部分展开,自然也有全局展开。其格式为using namespace 命名空间名。
#include<iostream>
#include<stdlib.h>
namespace ake
{int a = 1;int b = 2;
}using namespace ask;int main()
{printf("%d\n", a);printf("%d\n", b);return 0;
}
五、C++的输入和输出
C++的标准输入与输出函数是cin与cout,分别对应C语言的printf与scanf。但是相较于C语言,C++输入输出并不需要指定占位符,如:%d,%c等,输出可以⾃动识别变量类型。
#include<iostream>
using namespace std;//展开命名空间
int main()
{cout << "hello world" << endl;//endl相当于换行符cout << "hello world" << '\n';cout << 'a' << endl;int b = 1;cout << b << endl;cout << &b << endl;return 0;
}
注:在C++中使用 cin 与 cout 以及 endl 都属于C++标准库,需要包含头文件iostream以及std标准命名空间。
六、缺省参数
6.1 缺省参数的使用
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参缺省值,否则使用指定的实参。
void func(int a = 0)
{cout << a << endl;
}
int main()
{func(); // 没有传参时,使用参数的默认值,输出0func(1); // 传参时,使用指定的实参,输出1return 0;
}
运行结果:
0
1
6.2 缺省参数的分类
根据其缺省参数的个数,我们我可以将缺省参数分为全缺省与半缺省。
6.2.1 全缺省
每一个参数都有缺省值。
#include<iostream>
using namespace std;
void func(int a = 0, int b = 1, int c = 2)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;;
}
int main()
{func();//不穿参数func(10, 20);//半传参数func(10, 20, 30);//全传return 0;
}
6.2.2 半缺省
只有一部分参数有缺省值,并且半缺省参数必须从右往左依次来给出,不能间隔着给。同时传参只能从左往右依次传参。
#include<iostream>
using namespace std;
void func(int a ,int b=1,int c=2)
{cout <<"a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
int main()
{func(10,20);//半传参数cout << endl;func(10, 20, 30);//全传return 0;
}
5.2.3 注意
在使用缺省参数时,我们也要知道一些注意事项:
1. 传参时不能间隔传参
void func(int a ,int b=1,int c=2)
{cout <<"a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;
}
func(,10,20)//error
2. 缺省参数不能在函数的声明和定义同时出现
//test.h
void Func(int a = 10);//声明
// test.cpp
void Func(int a = 20)//定义
{}
七、函数重载
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
可以通过参数类型,参数个数,参数类型顺序来构成函数重载:
int Add(int a, int b)
{return a + b;
}
double Add(double a, double b)//类型不同
{return a + b;
}
int Add(int a, int b, int c)//个数不同
{return a + b;
}int Add(char a, int c)
{return a + c;
}
int Add(int a, char c)//类型顺序不同
{return a + c;
}
注意:
- 返回值类型不同无法构成函数重载
- 缺省值不同也不能构成函数重载
八、引用
8.1 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间 。其语法为:
引用对象类型& 引用变量名(对象名) = 引用实体;
引用类似于指针,因为指向同一块空间,所以改变引用变量引用实体也会改变。
#include<iostream>
using namespace std;
int main()
{int a = 1;int& b = a;//引用int& c = b;cout << &a << endl;cout << &b << endl;cout << &c << endl;c++;cout << a << endl;cout << b << endl;cout << c << endl;return 0;
}
8.2 引用的特性
8.2.1 引用时必须初始化
int& b;//错误的,必须初始化int& b = a;
8.2.2 一个变量可以有多个引用(给别名取别名)
int a = 1;
int& b = a;
int& c = a;//多个引用
8.2.3 引用一旦引用一个实体,再不能引用其他实体
int a = 1;
int& b = a;
b = 2;//这时是赋值,相当于a = b = 2;
8.3 引用的使用
8.3.1 作为函数的参数
#include<iostream>
using namespace std;void swap(int& a, int& b)
{int z = a;a = b;b = z;
}int main()
{int a = 1, b = 2;swap(a, b);cout << "a = " << a << endl;cout << "b = " << b << endl;return 0;
}
8.3.2 做函数返回值
函数的返回值是存储在一个临时的变量里面。这个变量正常情况下是不可修改的,可以看作一个常量,我们不能对常量进行赋值。
但使用引用作为返回值相当于返回一个引用,没有中间拷贝过程和临时变量,进而同时改变了引用对象(func1(a))和被引用对象(a)。
#include<iostream>
using namespace std;int& func1(int& a)
{a++;return a;
}int main()
{int a = 1;func1(a) = 10;cout << a;return 0;
}
8.3.3 错误示范
1. 引用指向的空间栈帧销毁
int& func()
{int a = 0;return a;
}
返回了a的引用,但当离开函数,函数的栈帧销毁,相当于返回了一个野指针
2. 引用指向的函数多次调用
int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << ret <<endl;return 0;
}//输出什么
输出结果为:7
那是因为在第二次调用函数Add(3,4)时,会在原来第一次调用Add(1,2)建立栈帧的空间上建立栈帧所以返回值c的值会被重新覆盖,ret是指向Add所在位置的栈帧的别名,所以ret值也会发生改变。
8.4 const引用
8.4.1 对常变量的引用(权限不可放大)
我们可以通过 const 修饰引用来让其变为常引用。这时引用变量是不能被修改的,并且只能将常变量复杂给常引用,不能将常变量赋值给引用,必须用const来引用。
const int a = 10;
// 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &”
// 这⾥的引⽤是对a访问权限的放⼤
//int& ra = a;//这样才可以
const int& a = 10;
// 编译报错:error C3892: “ra”: 不能给常量赋值
//ra++;
8.4.2 const引用普通变量(权限可缩小)
const 引⽤也可以引⽤普通变量,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
// 这⾥的引⽤是对b访问权限的缩⼩
int b = 20; const int& rb = b;
// 编译报错:error C3892: “rb”: 不能给常量赋值
//rb++;
8.4.3 const可以引用含有常性的对象
含有常性的变量包括常数,函数返回值等。
const int& ra = 30;
int a = 1, b = 2;
const int& ra = a * 3;
const int& rb = a + b;
double d = 12.34;
// 编译报错:“初始化”: ⽆法从“double”转换为“int &”
// int& rd = d;
int rc = d;//隐式类型转换
const int& rd = d;
不需要注意的是类似 int& ra = a*3; int& rb = a + b; int& rd = d; 这样⼀些场景下 a*3 的运算结果和 a 保存在⼀个临时对象中。 int& rd = d 也是类似,引用时进行类型转换被称为隐式类型转换,在类型转换中会产⽣临时对象存储中间值。也就是此时,ra 和 rd 引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥ 就触发了权限放⼤,必须要⽤常引⽤才可以。
所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
注:
const int a = 10;
int& ra = a;//权限放大
int rb = a;//权限没有放大
a处于一块临时对象,只拥有读取权限,没有写入权限,此时 int& ra 是指向a所在的空间(别名),要求读取和写入的权限,所以就产生了权限放大。第二个只是将 a 的值读取拷贝给 rb,并没有产生权限放大。
const 引用传参在未来学习模板类的时候会有进行运用。
8.5 引用与指针的区别
- 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个地址的变量,要开空间。
- 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
- 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
- sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
九、内联函数
在C语言中,无论宏常量还是宏函数虽能提升程序运行效,但都有易出错,无法调试等缺陷。而C++为了弥补这一缺陷,引入了内联函数的概念代替宏函数。
以关键字inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{return x + y;
}
int main()
{Add(1, 2);return 0;
}
vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅。
C/C++:常规——调试信息格式改成程序数据库,优化——内联函数扩展改成只适用于_inline
注意:
- 内联函数是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。内联函数的优势减少了调用开销,提高程序运行效率,缺陷就是可能会使目标文件变大。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不能声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
因为内联函数会在调用时直接展开,编译器默认认为不需要地址,如果声明与定义分离内联函数的地址根本不会进入符号表,链接时就无法找到定义的函数,就会发生链接错误。
十、nullptr
在C语言中,定义了一个宏NULL,在传统的C头文件(stddef.h)中,可以看到如下代码 :
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由此我们知道NULL既可以代表数字0,也可以代表空指针。这种模棱两可的定义就可能引出一些问题,比如下面这段代码:
#include<iostream>
using namespace std;
void func(int a)
{cout << "func(int)" << endl;
}
void func(int*p)
{cout << "func(int*)" << endl;
}
//函数重载
int main()
{func(0);func(NULL);func((int*)NULL);return 0;//输出??
}
我们的本意可能是将NULL当成一个指针,但是在默认情况下NULL被编译器当做数字0。这种问题是我们并不想看见的,所以C++11引入了nullptr来代替NULL。
C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。