人生中第一个C++程序
#include<iostream>
int main()
{
std::cout << "hello world!" << std::endl;
return 0;
}
1.C++关键字
根据(C++98)标准,C++总计63个关键字,C语言32个关键字。
Mirosoft visual stdio 2022标识:
2.命名空间
在C/C++中,变量,函数和类都是大量存在的,变量,函数,类的名称都将存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的:对表示的名称进行本地化。以避免命名冲突或名字污染。
关键字:namespace
关于命名冲突举例:
了解C语言都清楚malloc用于向计算机申请空间的函数。
#include<stdio.h>
//#include<stdlib.h>
int malloc = 0;
int main()
{
printf("%d\n", malloc);
return 0;
}
#include<stdio.h>
#include<stdlib.h>
int malloc = 0;
int main()
{
printf("%d\n", malloc);
return 0;
}
?
?当包含malloc所在的头文件时,原先的代码直接报错,如果是因为变量命名冲突导致报错,那就非常的搞人心态了。
C++贴心的使用命名空间搞掉了这个问题。
2.1命名空间定义
定义命名空间,需要使用关键字namespace,后面跟命名空间的名字,然后跟上一对{},{}中即为命名空间的成员。
举个栗子:
#include<iostream>
//普通的命名空间
namespace T1//以T1作为命名空间的名字
{
//内容中可以定义 变量,函数,结构体
char a;
void test()
{
printf("hello world\n");
}
struct ListNode
{
int val;
struct ListNode* next;
};
}
//进阶
//命名空间就像是一个“大函数”,函数能够嵌套,那么命名空间也能嵌套
namespace T2
{
char q;
namespace T3
{
int val;
}
}
//同一个工程中可存在多个名称相同的命名空间,编译器在链接时会将其自动合并
namespace T1
{
int num;
double e;
}
?
在namespace中定义的变量,函数,结构体等内容的作用域都将受限在干该命名空间中。
注意:在同一个域中,不能有同名变量。
2.2命名空间的使用
创建了命名空间,解决了可能存在的命名冲突问题,那么该怎么使用这些定义的变量。上文提及过,定义的所用内容的作用域都将受限于其命名空间中,所以直接在其他函数内是无法被使用的。
C++提供了操作符:作用域限定操作符::
1.加命名空间名称及作用域操作符
这就是老实人手动访问
2.使用using将命名空间中成员引入
?
这里是只把T1中的test函数放出来。
3.使用using namespace 命名空间名称引入
这里是将T1中定义的东西全部释放出来。
所以可以解决之前的疑问:为啥要在文件中加using namespace std;
如果不加呢?
可知,cout 和 endl 是包含在C++标准库中的,而且这个库还是一个单独
的命名空间。
在某些情况下,将库完全放出来会出现命名污染的情况,需要注意。
不将标准库中的所有放出来,可以考虑使 1 用和 2.
?
?
?
3.C++输入和输出
针对人生第一个C++程序,hello world的说明:
- 使用cout标准输出(控制台)和cin标准输入(键盘)时,必须包含<iostream>头文件以及std标准命名空间。
- 使用C++输入输出更方便,不需要加数据格式化控制。
4.缺省参数
有个备胎,走在路上都安心了好多(doge)
C++中的函数参数也是可以有备胎的。
4.1缺省函数的概念
缺省函数是生命或者定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则
采用默认值,否则使用指定的实参。
举例子:
void test(int i = 1)
{
cout << i << endl;
}
int main()
{
test();
test(20);
return 0;
}
?
?可见:没有传参时,使用参数为默认值,传参时,使用指定的实参。
4.2缺省参数分类
- 全缺省参数
void fun(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
fun();
return 0;
}
-
半缺省参数 void fun(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
fun(1,2);
return 0;
} ? 可知:这是一种部位,且这种部位是顺序的,即无法规定传递的实参2由c来接受。
注意:
1.版缺省参数必须从右往左依次来给出,不能间隔着。
2.缺省参数不能再函数声明和定义中同时出现。
3.缺省值必须是常量或者全局变量。
4.C语言不支持
5.函数重载
5.1概念
函数重载:函数的一个特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,
这些同名函数的形参列表(参数个数/类型/顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
举个栗子:
int Add(int a, int b)
{
return a + b;
}
double Add(double a, int b)
{
return a + b;
}
int main()
{
Add(1, 2);
Add(1.6, 2);
cout << "Add(1,2) = " << Add(1, 2) << endl;
cout << "Add(1.6,2) = " << Add(1.6, 2) << endl;
return 0;
}
?
这里可以看到:虽然调用了“相同的函数接口”,但是实际上调用的函数接口是不同的。
构成函数重载的关键:
参数个数,类型,顺序中的至少一个即可。
6.引用
6.1引用概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用的变量开辟内存空间,
它和它引用的变量共用一块内存空间。
比如:《西游记》里的猴子,敬称“齐天大圣”,法号“悟空”。指的都是同一人。
类型& 引用变量名(对象名)= 引用实体;
举个例子:
int main()
{
int a = 10;
int& b = a;
cout << "a = " << a<< endl;
cout << "b = " << b << endl;
cout << "a: " << &a << endl;
cout << "b: " << &b << endl;
b = 20;
cout << "a = " << a << endl;
return 0;
}
?
由此可以判定:引用并不是一种赋值操作,而是一种绑定操作。
6.2引用特性
1.引用在定义时必须初始化。
2.一个变量可以有多个引用。
3.引用一旦引用一个实体,就不能再引用其它实体。
int main()
{
int a = 10;
//int& b;//会报错
int& c = a;
int& d = c;
printf("%p\n%p\n%p\n",&a,&c,&d);
return 0;
}
?
6.3常引用
引用针对变量有一套语法,那对于常量语法还能使用吗?
答案是不能。
int main()
{
int a = 10;
int& b = a;
//那要直接给常量10取别名,而不是对变量a取别名。
//int& c = 10;//报错
//为什么?
//常量10只能读不能写,那直接引用会将其权限扩大,是不被允许的。
const int& c = 10;
const int& d = a;
return 0;
}
加上const 可以解决问题,那对于变量引用能否使用const修饰?
答案是可以的。
可以得知:
取别名原则:对原引用变量,权限(读写权限)只能缩小,不能放大。
再看一组代码:
int main()
{
double a = 1.6;
//int& b = a; //类型不一样,会报错。
const int& b = a;//不会报错
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
?
?
但是其中的机制又是什么?
a为浮点型,b为整形,之间的转换涉及类型转换,会生成一个临时变量存放在寄存器中。
我们引用的b实际上是那个临时变量的。
验证:
可见虽然b是a的引用,但是地址是不同的。
这里也可以得知:临时变量是具有常性的。
6.4使用场景
1.做参数
使用引用可以规避指针问题
初始C语言时:写过交换函数
void Swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(a ,b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
这个写法是错误的
交换了临时变量但实参却没有解决。但引用就可以完美解决。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 1, b = 2;
Swap(a ,b);
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
?
2.做返回值
函数返回值在出了作用域时,会销毁栈帧,原先的返回值会放在寄存器中(拷贝)
最后才会将结果赋值给你所创建在调用函数接口的变量中(拷贝)
int& f()
{
static int a = 0;
a++;
return a;
}
int main()
{
int b = f();
return 0;
}
传值返回:会出现一个拷贝。
传引用返回:没有这个拷贝了,函数返回的直接就是返回变量的别名。
这似乎能提高计算机的速度,但是有一个致命的问题,引用的原对象必须
”活着“,否则这个引用就会越界访问,是违法的。
这里使用了static将a放进了静态区,生命周期为整个程序的生命周期,当然不会出现越界访问。
体验临时变量生命周期结束的情况:
int& Add(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);//ret为c的别名
Add(3, 4);//再次调用函数,返回是被修改的c
cout << "Add(1,2) : " << ret << endl;//第一次打印
cout << "Add(1,2) : " << ret << endl;//第二次打印时,原先的c的栈帧已被销毁。
cout << "Add(1,2) : " << ret << endl;//同第二次
return 0;
}
?
所以:如果函数返回时,出了函数作用域,如果返回对象还未交还给系统,则可以使用引用返回,
如果已经返回给系统里,那就必须使用传值返回。
6.5针对值,指针和引用最返回值类型在性能上的比较
#include<time.h>
typedef struct Test
{
int Arr[100000];
}ST;
ST st;
ST test1()
{
return st;
}
ST& test2()
{
return st;
}
ST* test3()
{
return &st;
}
int main()
{
//值返回
size_t begin1 = clock();
for (int i = 0; i < 10000; i++)
{
test1();
}
size_t end1 = clock();
//引用返回
size_t begin2 = clock();
for (int i = 0; i < 10000; i++)
{
test2();
}
size_t end2 = clock();
//指针返回
size_t begin3 = clock();
for (int i = 0; i < 10000; i++)
{
test3();
}
size_t end3 = clock();
cout << "值返回:" << end1 - begin1 << endl;
cout << "引用返回:" << end2 - begin2 << endl;
cout << "指针返回:" << end3 - begin3 << endl;
return 0;
}
?
通过上述比较,可知指针返回和引用返回的效率差不多,值返回效率最差。
6.6引用和指针的区别
引用:在概念上引用只是原变量的一个别名,没有独立的空间,和引用实体公用同一块空间。
指针:要开辟空间,为4/8字节。
而底层实现上,引用是有空间的,因为引用是按照指针的方式实现的。
| 引用 | 指针 | 初始化 | 必须初始化 | 无要求 | 指向性 | 不能引用多个实体 | 可指向任一实体 | NULL | 无NULL引用 | NULL指针 | sizeof含义 | 引用类型的大小 | 指针地址空间所占字节个数 | 自加 | 实体增加1 | 向后偏移一个类型的大小 | 多级 | 无多级引用 | 有多级指针 | 访问实体方式 | 编译器自行处理 | 需要解引用 | 安全性 | 引用比指针使用起来更加安全 | |
查看引用和指针的汇编代码
7.内联函数
7.1概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在掉哦用内联函数的地方展开。
没有函数压栈的一系列消耗,内联函数可极大的提高效率。
根据内联函数的基本概念,可知其与C语言中的宏有相似之处。
对比C语言和C++内联函数
宏定义Add函数
#define ADD(X,Y) ((X) + (Y))
int main()
{
ADD(1, 2);
cout << ADD(1, 2) << endl;
return 0;
}
int main()
{
int ret = ADD(1, 2);
cout << ret << endl;
return 0;
}
//内联函数
inline int ADD(int x, int y)
{
return x + y;
}
int main()
{
int ret = ADD(1, 2);
cout << ret << endl;
return 0;
}
?
7.2特性
1.inline是一种以空间换时间的做法,省去函数调用栈帧的开销。所以代码很长或者有循环/递归的函数不适合作为内联函数。
2.inline对于编译器是一种建议,所以如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略内联。
3.inline不推荐声明和定义分离,分离会导致连接错误。因为inline被展开就没有函数地址了,链接时就会报错。
C++推出内联函数是为了解决C语言宏的晦涩难懂,容易出错,不方便调试的缺点。
优点:满足C语言宏的所有优点,
内联的写法与普通函数完全相同,极大的减少了编写的工程量。
【面试题】
宏的优缺点?
优点 | 缺点 | 增强代码的可读性 | 不方便调试宏(预编译阶段宏会被替换) | 提高性能 | 代码可读性变差,维护性变差,易被误用 | | 没有类型安全的检查 |
C++针对C语言宏的优化替代?
1.常量定义 换用const
2.函数定义 换用内联函数
8.auto关键字
8.1auto简介
在C/C++赋予auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但随着编译器的优化
auto似乎没再出现过。
C++11中,auto被赋予了全新的含义:auto不再是一个储存类型的指示符,而是作为一个新的
类型的指示符来指示编译器,auto生命的变量必须由编译器在编译时期推导而得。
int main()
{
int a = 10;
auto b = a;//int
auto c = 'a';//char
auto d = &a;//int*
auto* e = &a;//int*
auto& f = a;//int
//typeid(a).name();用于查看变量类型
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
cout << typeid(e).name() << endl;
cout << typeid(f).name() << endl;
//d = 20;
return 0;
}
?
注意: auto g;非法,g的类型无法被识别。
【注意】使用auto定义变量时必须对其进行初始化,在编译阶段需要根据初始化表达式来
推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的占位符,
编译器在预编译期将auto替换为变量实际的类型。
那auto是否可以作为缺省参数进行传参?
void AutoTest(auto a = 10)
{}
int main()
{
AutoTest();
return 0;
}
答案是不允许的。
8.2auto的使用细则
1.auto与指针和引用结合起来使用
用auto声明指针类型时,用auto或auto*没有任何区别,但用auto声明引用变量时必须加&
int main()
{
int a = 10;
auto b = &a;
auto* c = &a;
auto& x = a;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(x).name() << endl;
return 0;
}
?
2.在同一行定义多个变量
在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错。
原因:编译器实际只会对第一个类型进行推导,然后用推导出来的类型定义其他变量。
测试:
void AutoTest()
{
auto a = 10, b = 20;
auto c = 10.0, d = 20;
}
int main()
{
AutoTest();
return 0;
}
?
8.3auto不能推导的场景
- auto不能作为函数参数
- auto不能用来声明数组
- C++11只保留了auto作为类型指示符的用法
- auto最常用的是配合范围for循环,和lambda表达式配合使用。
9.基于范围的for循环(C++11)
9.1范围for的语法
以遍历数组为例:
C++98语法规则遍历
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(a)/sizeof(int); i++)
{
cout << a[i] << " ";
}
cout << endl;
return 0;
}
C++11语法规则范围for遍历
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9 };
for (auto e : a)//范围for (auto 变量 : 数组名}
{
cout << e << " ";
}
cout << endl;
return 0;
}
?对于一个有范围的集合而言,使用范围for时比较优的选择。
for循环后的括号由冒号”:“分为两部分:一部分时范围内用于迭代的变量,
第二部分则表示被迭代的范围。
注意:与普通循环类似,可以用continue来结束本次循环,也可以应break跳出整个循环。
9.2范围for的使用条件
1.for循环的迭代的范围必须是确定的。
对数组而言,就是数组中第一个元素和最后一个元素的范围;
对类而言,提供begin和end的范围,begin和end就是for循环迭代的范围。
错误示范:
void ForTest(int a[])
{
for (auto e : a)
{
cout << e << " " << endl;
}
cout << endl;
}
int main()
{
int a[] = { 1,2,3,4,5,6,7,8,9 };
ForTest(a);
return 0;
}
?
原因:数组传参传递的是首元素地址,没有直接可确定的范围,自然范围for就是错误的。
2.迭代器的对象实现++和==的操作。
10.指针空值nullptr(C++11)
10.1C++98中的指针空值
在C++98中可以这样定义空指针
NULL实际上是一个宏,包含在C头文件(stddef.h)中,转到定义可知:
NULL可能被定义为字面常量0或者被定义为(void*)常量,但这可能会出现指代不明的情况。
void NULLTest(int)//字面常量
{
cout << "int" << endl;
}
void NULLTest(int*)//(void*)常量
{
cout << "int*" << endl;
}
int main()
{
NULLTest(0);//~int
NULLTest(NULL);//希望调用int*
NULLTest((int*)NULL);//~~int*
NULLTest(nullptr);
return 0;
}
?
可以看到,事与愿违,NULL调用的是int类型的字面常量。
在C++98中,字面常量既可以是一个整型数字,也可以是个无类型的指针(void*)常量,
编译器默认情况下将其看成一个整型常量,如果要按照指针的方式来使用,必须对其使用
强转。
C++11推出了新的nullptr来替代原有的NULL/0.
注意:
1.在使用nullptr表示指针为空时,不与要包含头文件,因为nullptr在C++11版本下是关键字。
2.在C++中,sizeof(nullptr)与sizeof((void*))所占字节相同。
3.为了提高代码的健壮性,nullptr在c++中可以完全替代NULL/0.
如有错误,还请大佬指出!!
|