人生中第一个C++程序
- #include<iostream>
- int main()
- {
- std::cout << "hello world!" << std::endl;
- return 0;
- }
根据(C++98)标准,C++总计63个关键字,C语言32个关键字。
Mirosoft visual stdio 2022标识:
在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++贴心的使用命名空间搞掉了这个问题。
定义命名空间,需要使用关键字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中定义的变量,函数,结构体等内容的作用域都将受限在干该命名空间中。
注意:在同一个域中,不能有同名变量。
创建了命名空间,解决了可能存在的命名冲突问题,那么该怎么使用这些定义的变量。上文提及过,定义的所用内容的作用域都将受限于其命名空间中,所以直接在其他函数内是无法被使用的。
C++提供了操作符:作用域限定操作符::
1.加命名空间名称及作用域操作符
这就是老实人手动访问
2.使用using将命名空间中成员引入
这里是只把T1中的test函数放出来。
3.使用using namespace 命名空间名称引入
这里是将T1中定义的东西全部释放出来。
所以可以解决之前的疑问:为啥要在文件中加using namespace std;
如果不加呢?
可知,cout 和 endl 是包含在C++标准库中的,而且这个库还是一个单独
的命名空间。
在某些情况下,将库完全放出来会出现命名污染的情况,需要注意。
不将标准库中的所有放出来,可以考虑使 1 用和 2.
针对人生第一个C++程序,hello world的说明:
有个备胎,走在路上都安心了好多(doge)
C++中的函数参数也是可以有备胎的。
缺省函数是生命或者定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则
采用默认值,否则使用指定的实参。
举例子:
-
- void test(int i = 1)
- {
- cout << i << endl;
- }
-
- int main()
- {
- test();
- test(20);
- 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();
- 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语言不支持
函数重载:函数的一个特殊情况,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;
- }
这里可以看到:虽然调用了“相同的函数接口”,但是实际上调用的函数接口是不同的。
构成函数重载的关键:
参数个数,类型,顺序中的至少一个即可。
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用的变量开辟内存空间,
它和它引用的变量共用一块内存空间。
比如:《西游记》里的猴子,敬称“齐天大圣”,法号“悟空”。指的都是同一人。
类型& 引用变量名(对象名)= 引用实体;
举个例子:
- 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;
- }
由此可以判定:引用并不是一种赋值操作,而是一种绑定操作。
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;
- }
引用针对变量有一套语法,那对于常量语法还能使用吗?
答案是不能。
- 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的引用,但是地址是不同的。
这里也可以得知:临时变量是具有常性的。
使用引用可以规避指针问题
初始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;
- }
函数返回值在出了作用域时,会销毁栈帧,原先的返回值会放在寄存器中(拷贝)
最后才会将结果赋值给你所创建在调用函数接口的变量中(拷贝)
- 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;
- }
所以:如果函数返回时,出了函数作用域,如果返回对象还未交还给系统,则可以使用引用返回,
如果已经返回给系统里,那就必须使用传值返回。
- #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;
- }
通过上述比较,可知指针返回和引用返回的效率差不多,值返回效率最差。
引用:在概念上引用只是原变量的一个别名,没有独立的空间,和引用实体公用同一块空间。
指针:要开辟空间,为4/8字节。
而底层实现上,引用是有空间的,因为引用是按照指针的方式实现的。
| 引用 | 指针 |
初始化 | 必须初始化 | 无要求 |
指向性 | 不能引用多个实体 | 可指向任一实体 |
NULL | 无NULL引用 | NULL指针 |
sizeof含义 | 引用类型的大小 | 指针地址空间所占字节个数 |
自加 | 实体增加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;
- }
1.inline是一种以空间换时间的做法,省去函数调用栈帧的开销。所以代码很长或者有循环/递归的函数不适合作为内联函数。
2.inline对于编译器是一种建议,所以如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略内联。
3.inline不推荐声明和定义分离,分离会导致连接错误。因为inline被展开就没有函数地址了,链接时就会报错。
C++推出内联函数是为了解决C语言宏的晦涩难懂,容易出错,不方便调试的缺点。
优点:满足C语言宏的所有优点,
内联的写法与普通函数完全相同,极大的减少了编写的工程量。
【面试题】
宏的优缺点?
优点 | 缺点 |
增强代码的可读性 | 不方便调试宏(预编译阶段宏会被替换) |
提高性能 | 代码可读性变差,维护性变差,易被误用 |
| 没有类型安全的检查 |
C++针对C语言宏的优化替代?
1.常量定义 换用const
2.函数定义 换用内联函数
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;
- }
答案是不允许的。
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;
- }
- auto不能作为函数参数
- auto不能用来声明数组
- C++11只保留了auto作为类型指示符的用法
- auto最常用的是配合范围for循环,和lambda表达式配合使用。
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跳出整个循环。
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.
如有错误,还请大佬指出!!