【C++】——入门基础
文章目录
前言
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等
从本篇开始我们就进入C++的学习,现在我们开始学习吧
1. C++初识
1.1 第一个c++程序
C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行,C++中需要把定义文件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要用g++编译,不再是gcc
我们先来看看这两个版本是如何输出 hello world 的
C版本的hello world应该是这样写的
// test.c
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
C++版本的hello world应该是这样写的
// test.cpp
#include<iostream> // 导入头文件
using namespace std; // 简化对命名空间std下函数和对象的使用
// 主函数,c++程序的执行入口
int main() {
cout << "Hello world" << endl; // 在终端输出文本 “Hello world” 并换行
return 0; // 表示程序正常结束,返回给操作系统的退出码为0
}
我们说C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行
所以C++版本的hello world也可以这样写的
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
1.2 注释
作用:在代码中加一些说明和解释,方便自己或其他程序员阅读代码
两种格式:
- 单行注释:通常放在一行代码的上方,或者一条语句的末尾,对该行代码说明
- 多行注释 :通常放在一段代码的上方,对该段代码做整体说明
1.3 变量
作用:给一段指定的内存空间起名,方便操作这段内存
示例:
#include<iostream>
using namespace std;
int main() {
// 变量的定义
// 数据类型 变量名 = 初始值
int a = 10;
cout << "a = " << a << endl;
return 0;
}
注意:C++在创建变量时,必须给变量一个初始值,否则会报错
1.4 常量
作用:用于记录程序中不可更改的数据
两种格式:
- #define 宏常量:通常在文件上方定义,表示一个常量
- const修饰的变量:通常在变量定义前加关键字const,修饰该变量为常量,不可修改
示例:
#include<iostream>
using namespace std;
//1、宏常量
#define day 7
int main() {
cout << "一周里总共有 " << day << " 天" << endl;
//day = 8; //报错,宏常量不可以修改
//2、const修饰变量
const int month = 12;
cout << "一年里总共有 " << month << " 个月份" << endl;
//month = 24; //报错,常量是不可以修改的
return 0;
}
1.5 关键字
作用:关键字是C++中预先保留的单词(标识符)
在定义变量或者常量时候,不要用关键字

具体用法可以先看看这个链接:C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)
1.6 标识符命名规则
作用:C++规定给标识符(变量、常量)命名时,有一套自己的规则:
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
2. 命名空间
2.1 namespace的价值
C语言中的命名冲突
那么如果我们定义的变量名字与库函数中的变量或者函数名一样起冲突了怎么办
#include<stdio.h>
#include<stdlib.h>
int abs = 10;
int main()
{
printf("%d\n", abs);
return 0;
}

我们会发现这会报错,为什么呢,我们来看看
abs() 是 stdlib.h文件中定义的函数,如果我们定义到全局域,就会与库函数中的变量或者函数名冲突。C语言没办法解决类似这样的命名冲突问题
那不可以放到局部域吗?当然可以
#include<stdio.h>
#include<stdlib.h>
int main()
{
int abs = 10;
printf("%d\n", abs);
return 0;
}
因为遵循就近原则,所以程序会优先搜索局部域,找不到才会去搜索全局域(C语言的知识),所以写到局部域就不会报错,但是C++有更好的解决方案
实际应用中,一个项目往往是多人合作完成的,很有可能会出现多个人写的函数名或者变量名一样,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的
2.2 命名空间的定义
在namespace关键字后面加上命名空间的名字,然后加一对{},{}中即为命名空间的成员。
命名空间中还可以定义变量/函数/类型。
#include<stdio.h>
#include<stdlib.h>
namespace qike
{
int abs = 10;
int func(int x)
{
return x + 1;
}
struct L
{
int date;
int* next;
};
}
int main()
{
printf("%d\n", czh::abs);
return 0;
}
注意:命名空间只能在全局域定义,不能在函数内部定义!
命名空间也可以嵌套使用
namespace qike
{
int abs = 10;
int func(int x)
{
return x + 1;
}
namespace qike1
{
int a = 0;
int Add(int x, int y)
{
return x + y;
}
}
}
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
因为本质是一个命名空间,所以不能存在相同变量/函数/类型,也就是相同命名空间内部不能出现命名冲突,举几个例子吧
- 函数重载:
namespace qike {
void func() {}
}
namespace qike {
void func(int) {} // 合并后没有冲突
}
- 类型定义:
namespace qike {
struct MyStruct {};
}
namespace qike {
struct MyOtherStruct {}; // 合并后没有冲突
}
- 变量:
namespace qike {
int value = 5;
}
namespace qike {
int value = 10; // 这里会导致冲突,编译错误
}
2.3 命名空间的使用
C语言中只有两个作用域:局部域和全局域。
C++中有四个作用域:局部域、全局域、命名空间域、类域。
域作用限定符:两个冒号 :: 后面访问命名空间域和类域时会经常用到。
之前在C语言中,这个符号也是用来解决命名冲突的问题。
int a = 100;
int main()
{
int a = 11;
printf("%d\n", a);//打印11
printf("%d\n", ::a);//打印100
return 0;
}
程序会优先搜索局部域,再搜索全局域,如果局部域和全局域变量名一样,那么访问局部域就需要用到域作用限定符 ::
举个例子
namespace qike
{
int x = 10;
int y = 20;
int Add(int a, int b)
{
return a + b;
}
}
- 加命名空间名称及作用域限定符:
int main()
{
printf("%d\n", qike::x);
printf("%d\n", qike::y);
int a = 1, b = 2;
printf("%d\n", qike::Add(a, b));
return 0;
}
- 使用关键字
using将命名空间中某个成员引入
using qike::x;
using qike::y;
using qike::Add;
int main()
{
printf("%d\n", x);
printf("%d\n", y);
int a = 1, b = 2;
printf("%d\n", Add(a, b));
return 0;
}
- 使用
using namespace命名空间名称引入
using namespace qike;
int main()
{
printf("%d\n", x);
printf("%d\n", y);
int a = 1, b = 2;
printf("%d\n", Add(a, b));
return 0;
}
注意:展开命名空间域后,全局定义和命名空间域的定义会不明确。
int a = 2;
namespace qike
{
int a = 1;
}
using namespace qike;
int main()
{
printf("%d\n", a);
return 0;
}

还有一种特殊的命名空间:无名的命名空间
namespace
{
int a = 10;
int b = 20;
}
int main()
{
printf("%d\n",a);
return 0
}
可以不通过域作用限定符(也无法通过)直接访问命名空间中的成员。具有内部链接性,不能在命名空间所属文件以外的地方使用该命名空间里面的成员
std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中,也就是我们常见的
using namespace std;
我们日常练习的时候推荐这样直接展开std,但是以后写项目的时候不建议直接将标准命名空间展开,否则自己定义的东西容易跟库里的起冲突,推荐用下面两种写法,比较安全
using std::cin;
using std::cout;
using std::endl;
int main()
{
int a;
cin >> a;
cout << a << std::endl;
return 0;
}
也可以边使用边展开
int main()
{
int a;
std::cin >> a;
std::cout << a << std::endl;
return 0;
}
小结:
-
不同命名空间域可以有同名变量(本质是两个变量),同命名空间域中不能有同名变量,可以用嵌套命名空间域解决
-
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中,但是不可有同名变量
-
程序会优先搜索局部域,其次全局域,默认不会搜索命名空间域,需要用上述三种方法来访问命名空间中的成员
-
命名空间只能在全局域定义,不能在函数内部定义
3. C++输入&输出
- 流插入运算符:<<
- 流提取运算符:>>
- cin:标准输入对象
- cout:标准输出对象
cout 和 cin 是全局的流对象,endl 是特殊的C++符号,表示换行输出,他们都包含在包含<iostream>头文件中,使用时按命名空间的使用方法使用std
#include <iostream>
using namespace std;//或者using std::cout;
int main()
{
cout << "qike" << endl;
return 0;
}
也可以这样写
#include <iostream>
int main()
{
std::cout << "qike" << endl;
return 0;
}
C语言的printf、scanf需要手动控制格式
C++的输入输出可以自动识别变量类型,使用更方便
#include<iostream>
using namespace std;
int main()
{
int x;
double y;
char z[10];
cin >> x >> y >> z;
cout << x << endl;
cout << y << endl;
cout << z << endl;
return 0;
}

但是并不是任何情况下都方便使用,比如控制浮点数输出精度,控制整形输出进制格式等,使用的并不多,因为这些使用C语言实现更简单
例如C++控制浮点数输出精度
#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
double x = 1.23456789;
cout << x << endl;// 默认精度为6位有效数
cout << fixed << setprecision(8) << x << endl;
return 0;
}

cout 默认保留6位有效数字,并且会自动删去小数点结尾的0,使用 cout 控制输入小数点位数需要按照上述方法,并且包含头文件 iomanip,使用起来麻烦,远没有 scanf 简单
printf("%.8lf\n", x);
注意:并且在一些算法题中,数据量比较大的情况下,scanf 和 printf 要比 cin 和 cout 的效率高,使用 cin 和 cout 可能会超时。
总而言之,哪个方便用哪个,也可以混合使用,C++是向下兼容C语言的,可以使用C语言的的代码
4. 缺省参数
C语言是不支持缺省参数的
缺省参数概念:缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
- 全缺省参数:全部参数都有缺省值
#include<iostream>
using namespace std;
void Func(int a = 1, int b = 2, int c = 3)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10, 20, 30); // 传参时,使用指定的实参
Func(10, 20); // 传几个就用几个
return 0;
}

- 半缺省参数:只有部分参数有默认值
#include<iostream>
using namespace std;
void Func(int a, int b = 2, int c = 3)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
int main()
{
// Func(); // 第一个参数必须传参
Func(10);
Func(10, 20, 30); // 传参时,使用指定的实参
Func(10, 20); // 传几个就用几个
return 0;
}

注意:
- 半缺省参数必须从右往左依次来给,不能间隔着给,这是因为函数参数时是从左往右依次传参的
错误写法:
void Func(int a = 10, int b, int c = 30)
{
//...
}
void Func(int a = 10, int b, int c)
{
//...
}
- 缺省参数不能在函数声明和定义中同时出现,声明给缺省参数,定义不给

- 缺省值必须是常量或者全局变量
int x = 100;
int y = 200;
void Func(int a = x, int b = y, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
5. 函数重载
函数重载:在C++中,同一个作用域下,多个函数的函数名字相同,形参列表不同,称为函数重载,常用来处理功能类似而数据类型不同的问题
- 参数类型不同构成函数重载
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
- 参数个数不同构成参数重载
void Func()
{
cout << "Func()" << endl;
}
void Func(int x)
{
cout << "Func(int x)" << endl;
}
- 参数类型顺序不同构成重载
void Func(int x, double y)
{
cout << "Func(int x, double y)" << endl;
}
void Func(double x, int y)
{
cout << "Func(double x, int y)" << endl;
}
注意:返回值类型不同不会构成函数重载
以下两个函数构成重载,但是调用会出现问题,无参调用存在歧义
void Func()
{
cout << "Func()" << endl;
}
void Func(int x = 0)
{
cout << "Func(int x = 0)" << endl;
}
接下来我们来了解一下,C语言为什么不能实现函数重载?
底层原因是C语言和C++在编译链接阶段对函数名字的修饰规则不同
C语言是直接通过函数名去查找,重载函数的函数名是相同的,所以存在歧义
而C++对函数名字的修饰规则与参数有关,参数不同,修饰出来的名字就不一样,因此支持函数重载
小结:
- 函数重载必须满足参数列表不同:参数个数不同/参数类型不同/类型顺序不同
- 返回值类型不同不构成函数重载,因为C++对函数名的修饰规则与返回值无关
6. 引用
6.1 引用的概念和定义
引用是给变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块空间,所以一个发生改变,两个都会改变
使用方法:类型& 引用变量名(对象名) = 引用实体;
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int& b = a;
cout << &a << endl;
cout << &b << endl;
b = 2;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}

6.2 引用的特性
- 引用在定义时必须初始化
int a = 1;
//int& ra; 错误写法
int& ra = a;
- 一个变量可以有多个引用
int a = 1;
int& b = a;
int& c = a;
- 一个引用变量只能在初始化时引用一个实体,后面不能再引用其他实体
int main()
{
int a = 1;
int& b = a;
int& c = a;
cout << a << endl;
cout << b << endl;
cout << c << endl;
int x = 100;
b = x;// 不是引用x, 而是x赋值给b;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}

可以看到,执行 b = x 之后,a,b,c的值都发生了改变。这是因为a,b,c公用一块空间,将x赋值给b,也就改变了这块空间的内容,相当于同时赋值给a和c
- 不能引用常量
int& ra = 10;
常引用:
const int x = 0;
int y = x; // y是x的拷贝,没有放大权限,因为y的改变不影响
const int a = 1;
int& ra = a; // 权限不可以放大,a是不可变的
const int& ra = a; // 权限平移
int b = 2;
const int& rb = b; // 权限可以缩小
++rb; // 不能通过rb来修改,因为是常引用
++b;
引用变量类型和实体类型不同,要使用常引用
double d = 3.14;
int& y = d;
const int& z = d;

隐式类型转换会产生一个临时变量,临时变量具有常性,所以要用常引用
6.3 引用的使用
- 引用做参数
int Swap(int& x, int& y)
{
int temp = x;
x = y;
y = x;
}
- 引用做返回值
值返回会产生一个临时变量,返回结果赋给这个临时变量,再将这个临时变量赋给接收的变量ret
由于临时变量具有常性,所以要用常引用接收
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int& ret1 = Add(10, 20);
const int& ret2 = Add(10, 20);
return 0;
}

但是上述代码还有问题,需要与下面的代码放到一起说明
引用返回不会产生临时变量返回,直接返回结果的引用也就是别名
int& Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int ret1 = Add(10, 20);
return 0;
}

但是,上面两段代码都存在潜在问题:ret的打印结果是不确定的
Add函数结束,栈帧会销毁,如果没有清理栈帧,那么ret的结果侥幸是正确的;如果清理了栈帧,那么返回结果z的空间会被系统回收,ret的结果就会是随机值
如果引用接收函数的返回结果,再次调用函数,ret的结果也会发生改变。调用完函数完,栈帧销毁,再次调用该函数会使用同一块空间,所以结果就会被覆盖
函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
因此我们可以用static来修饰返回结果,延长其生命周期至main()结束
int& Add(int x, int y)
{
static int z = x + y;
return z;
}
或者是返回全局变量
int n;//全局变量
int& Add(int x, int y)
{
n = x + y;
return n;
}
引用做返回值可以修改返回值
int n = 0;//全局变量
int& Func(int x)
{
n = 10 * x;
return n;
}
int main()
{
Func(1) = 11;
cout << n << endl;//11
return 0;
}
传值、传引用效率比较:以值作为参数或返回值类型,会先生成临时拷贝再返回,因此效率是非常低的,尤其是当参数或返回值类型非常大时
小结:
- 引用传参的使用场景非常广泛,基本任何场景都可以用引用传参
- 谨慎用引用做返回值。出了函数作用域,对象不存在就不能用引用返回,还在就可以用引用返回
- 引用做返回值可以修改返回值
- 引用返回可以减少拷贝提高效率
我们再来看看引用和指针的区别
- 概念上,引用是引用实体的别名,与其共用一块空间,而指针存储一个变量的地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位平台下占8个字节)
- 引用++则引用和实体都++,指针++则指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
7. 内联函数
内联函数概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,能够提升程序运行的效率
常规函数会调用call指令来调用函数

内联函数直接在使用的地方展开函数体,没有函数调用的开销
内联函数特性
- 内联函数是拿空间换取时间提高程序运行效率的,没有函数调用建立栈帧的开销。代码过长不适合用inline(否则目标文件会很大),内联函数适用于短小频繁调用的函数
- 内联函数对于编译器来说只是一个建议,最终是否成为内联函数,取决于编译器。不同编译器关于inline的实现机制可能不同。一般代码较长的函数或者递归函数,即使加了inline也被会否决掉。(vs默认debug模式下inline不会起作用,需要手动设置或切换release模式)
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
8. auto关键字
C++11规定:auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
auto可以根据表达式右边的类型自动推导左边的类型
int a = 0;
auto b = a;//int b = a;
int* p = &a;
auto q = p;//int* q = &a;
auto ch = 'a';//char ch = 'a';
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型
auto x;//错误!
auto使用规则:
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int x = 0;
auto p1 = &x;
auto* p2 = &x;
auto& rx = x;
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错
auto a = 1, b = 2;
auto c = 3, d = 3.14;//报错
- auto不能做函数的参数
void Test(auto x)//错误,编译器无法推导x的实际类型
{}
- auto不能直接声明数组
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
auto b[] = { 1, 2, 3 };
return 0;
}

- for循环与auto的结合使用
for循环后的括号由冒号 :分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
int arr[] = { 1,2,3,4,5 };
for (auto& x : arr)//改变数组中的内容要使用引用
{
x++;
}
for (auto x : arr)
{
cout << x << ' ';
}
以下代码就有问题,因为for的范围不确定,因为数组的大小并不会自动确定
void Print(int arr[])
{
for (auto& e : arr)
cout << e << endl;
}
9. 指针空值nullptr
之前C语言中学的NULL实际是一个宏,定义在传统的C头文件(stddef.h)中
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0
C++11中新引入关键字 nullptr 来解决这一问题。使用时无需包含头文件
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}

结语
这块入门知识到这里就结束了,还是比较轻松的
大家还是需要自己动手敲一敲代码加深自己的理解
好了,感谢你能看到这里,溜了溜了,我们下期再见吧
1万+




被折叠的 条评论
为什么被折叠?



