👉 欢迎阅读这篇文章 👇
目录
- 1、C++是什么
- 2、C++的第一个程序
- 3、命名空间
-
- [3.1 namespace的价值](#3.1 namespace的价值)
- 3.2namespace定义
- 3.3命名空间的使用
- 4、C++输入/输出
- 5、缺省参数
- 6、函数重载
- 7、引用
- 8、inline
- 9、nullptr
1、C++是什么
C++ 是一门静态类型、编译型的通用编程语言,由 Bjarne Stroustrup 在 C 语言基础上扩展而来,兼容 C 语言语法。
它融合了面向过程、面向对象、泛型编程多种编程范式,兼顾运行效率与代码抽象能力。底层可直接操作内存,上层支持封装、继承、多态与 STL 容器。
广泛用于游戏开发、后端服务、嵌入式、高性能计算、操作系统、音视频等对性能要求高的场景。
2、C++的第一个程序
C++兼容C语言绝大多数的语法,所以C语⾔实现的hello world依旧可以运⾏,C++中需要把定义文件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译。其他平台也有相关的C++编译器,比如linux下是g++。
cpp
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
当然C++有⼀套⾃⼰的输⼊输出,我们后面会了解到。
3、命名空间
3.1 namespace的价值
在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题。
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: "rand": 重定义;以前的定义是"函数"
printf("%d\n", rand);
return 0;
}
3.2namespace定义
定义命名空间,需要用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不存在冲突。
C++中有函数局部域 、全局域 、命名空间域 ,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑。
编译时查找声明或定义,若不指定域则默认只会先在局部域找,再去全局域找。若指定了域,则会直接去指定的域里去找。
有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
namespace只能定义在全局。
域作用限定符:::,用于指明名字属于哪个作用域。
cpp
#include <stdio.h>
#include <stdlib.h>
namespace chen
{
int rand = 10;
int Add(int a, int b)
{
return a + b;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
//打印的是rand函数指针的地址
printf("%p\n", rand);
//打印的是命名空间chen里的rand变量
printf("%d\n", chen::rand);
return 0;
}
- namespace可以嵌套定义
cpp
//命名空间可以嵌套定义
#include <stdio.h>
#include <stdlib.h>
namespace chen
{
namespace chen1
{
int rand = 1;
int Add(int x, int y)
{
return x + y;
}
}
namespace chen2
{
int rand = 2;
int Div(int x, int y)
{
return x / y;
}
}
}
int main()
{
//打印chen中的chen1中的rand变量
printf("%d\n", chen::chen1::rand);
//打印chen中的chen2中的rand变量
printf("%d\n", chen::chen2::rand);
return 0;
}
- 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
cpp
//Stack.h
#include<stdbool.h>
#include<assert.h>
namespace chen {
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps, int n);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
STDataType STTop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
}
//Stack.cpp
#include "Stack.h"
namespace chen {
void STInit(ST* ps, int n)
{
assert(ps);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
// 栈顶
void STPush(ST* ps, STDataType x)
{
assert(ps);
// ...
}
// ...
}
//main.cpp
//多个文件同名定义
#include "Stack.h"
int main()
{
chen::ST S1;
chen::STInit(&S1, 4);
chen::STPush(&S1, 5);
return 0;
}
- C++标准库都放在⼀个叫std(standard)的命名空间中。
cpp
//C++标准库都放在命名空间std中
#include <iostream>
int main()
{
std::cout << "hello" << std::endl;
return 0;
}
3.3命名空间的使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
- 指定命名空间访问(
::),项⽬中推荐这种⽅式。 - using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
- 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
cpp
//指定命名空间访问
#include<stdio.h>
namespace N
{
int a = 0;
int b = 1;
}
int main()
{
printf("%d\n", N::a);
return 0;
}
cpp
//using将命名空间中的某个成员展开
#include<stdio.h>
namespace N
{
int a = 0;
int b = 1;
}
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
cpp
//using将整个命名空间展开
#include<stdio.h>
namespace N
{
int a = 0;
int b = 1;
}
using namespace N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
4、C++输入/输出
<iostream>是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
std::cin是istream类的对象 ,它是⾯向窄字符(narrow characters (of type char))的标准输⼊流。
std::cout 是 ostream 类的对象 ,它是⾯向窄字符的标准输出流。
std::endl是操纵符, 底层实现是⼀个函数,流插入输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型 ,C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含<iostream>中间接包含了。 (vs系列编译器是这样的,其他编译器可能会报错)。
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << " " << c << endl;
std::cout << a << " " << b << " " << c << std::endl;
scanf("%d%lf", &a, &b);
printf("%d %lf\n", a, b);
cin >> a;
cin >> b >> c;
cout << a << endl;
cout << b << endl;
cout << c << endl;
return 0;
}
cpp
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
5、缺省参数
缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省 和半缺省 参数。缺省参数 也叫默认参数。
全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
cpp
#include <iostream>
using namespace std;
//普通缺省
void Func(int a = 0)
{
cout << a << endl;
}
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
// 半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
Func1();//没有传参时,使⽤参数的默认值
Func1(1);//传了一个参数,第一个参数使用指定的实参,剩下的,使⽤参数的默认值。
Func1(1, 2);//传了两个参数,前两个个参数使用指定的实参,剩下的,使⽤参数的默认值。
Func1(1, 2, 3);//传了三个参数,使用指定的实参。
Func2(100);//传了一个参数,第一个参数使用指定的实参,剩下的,使⽤参数的默认值。
Func2(100, 200);//传了两个参数,前两个个参数使用指定的实参,剩下的,使⽤参数的默认值。
Func2(100, 200, 300);//传了三个参数,使用指定的实参。
return 0;
}
6、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同也可以是参数类型不同,这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
cpp
//函数重载
#include<iostream>
using namespace std;
//1、参数类型不同
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;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
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;
}
// 下⾯两个函数构成重载
// 但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
// 返回值不同不能作为重载条件,因为调⽤时也⽆法区分
//void fxx()
//{}
//
//int fxx()
//{
// return 0;
//}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
7、引用
7.1引用的概念和定义
引用不是新定义一个变量,而是给已存在的变量取别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。
类型& 引⽤别名 = 引⽤对象;
cpp
#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引用的特性
- 引用在定义时必须初始化。
- 一个变量可以有多个引用。
- 引用一旦引用了一个实体,则不能再引用其他实体。
7.3引用的使用
引⽤在实践中主要是于引⽤传参 和传引⽤返回中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
传值返回和传引用返回
- 传值返回 会产⽣临时对象作为函数调⽤表达式的返回值。
- 传引⽤返回 本质是返回返回对象的引⽤(别名) ,从⽽提⾼了效率,同时可以改变返回对象。(不是所有场景都可以传引⽤返回,如果是⼀个返回值是当前函数的局部对象,传引⽤返回是有⻛险的)
cpp
#include<iostream>
#include <stdio.h>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
// 满了, 扩容
if (rs.top == rs.capacity)
{
printf("扩容\n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
// int STTop(ST& rs)
int& STTop(ST& rs)//传引用返回
{
assert(rs.top-1 > 0);
return rs.a[rs.top-1];
}
int main()
{
// 调⽤全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
STTop(st1) += 10;
cout << STTop(st1) << endl;
return 0;
}
对于STTop(st1) += 10;,由于这里运用了引用返回,所以可以让其返回值作左值,如果使用传值返回,那么返回的是返回值的拷贝副本,不能做左值。
7.4const引用
若一个对象被const修饰了,那么仍然可以引用,需要用const来引用。const引用也可以引用普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
cpp
//const引用
int main()
{
const int a = 0;
//不可以
//int& p = a;//引用------访问权限不可以放大
//需要const引用
const int& r = a;//引用------访问权限可以平行
int b = 0;
//可以const引用
const int& r1 = b;//引用------访问权限可以缩小
//不可以
//int* p1 = a;//指针------访问权限不可以放大
//需要const
const int* p = &a;//指针------访问权限可以平行
//可以const
const int* p1 = &b;//指针------访问权限可以缩小
// 指针和引用才有权限的放大和缩小这一说法
int c = a;//这里并没有权限的放大,赋值是一种拷贝,本质a拷贝给c,c的改变不影响a
//在类型转换、表达式求值后会产生临时对象存储中间值,re、rb引用的是临时对象,临时对象具有常性,所以要const引用
int d = 1;
const double& re = d;//先将int类型转换成double类型并存储到临时对象中,再引用
const int& rb = b * 3;//b*3得出结果后存储到临时对象中,再引用
return 0;
}
临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
注意只有指针和引用才有权限的放大和缩小这一说法。
对于const引用,形参里若是一个不需要改变的对象,则我们一般会使用const引用,一方面可以提高效率 ,另一方面,若形参里是一个const引用,那么实参就可以是三种
- 1.普通type类型(访问权限可以缩小)
- 2.const type类型
- 3.支持类型转换的临时对象(常性)
7.5指针和引用的关系
C++中指针和引⽤在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
- 在语法概念上引用是给一个变量取别名,不开辟空间;指针是存储⼀个变量地址,要开空间。
- 引⽤在定义时必须初始化 ;指针建议初始化,但是语法上不是必须的。
- 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象 ;指针可以不断地改变指向对象。
- 引⽤可以直接访问 指向对象;指针需要解引⽤才能访问指向对象。
- sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩ ;指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
- 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
8、inline
用inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率。
inline对于编译器⽽⾔只是⼀个建议,也就是说就算是加了inline,编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。
inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline可能也会被编译器忽略。
C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数。
vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试。release版本会展开inline。
inline声明和定义不要分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
cpp
//inline------内联函数
//宏函数
//#define Add(int x,int y) return x+y;//错误
//#define Add(x,y) return x+y;//错误
//#define Add(x,y) x+y//错误
//#define Add(x,y) (x+y);//错误
//思考:为什么不能加分号? 宏函数会在main函数被替换掉,如果加分号会出现分号重叠。
// 为什么要加外面括号? 如果在main函数里出现int ret = Add(x, y)*2;就会因为优先级问题出错。
// 为什么要加里面的括号? 如果x和y是一个表达式,比如Add(a&b, a|c),+的优先级大于表达式里的符号优先级的情况,会出错。
#define Add(x,y) ((x)+(y))
//inline函数
inline int ADD(int x, int y)
{
return x + y;
}
int main()
{
int x = 10;
int y = 20;
int ret = Add(x, y);
int ret1 = ADD(x, y);
}
cpp
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
// 链接错误:⽆法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
f(10);
return 0;
}
9、nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
c
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
cpp
#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);
f(nullptr);
return 0;
}