昆明做网站哪家好/app推广拉新接单平台
C++入门
- 前言
- 1. C++关键字(C++98)
- 2. 命名空间
- 2.1 命名空间定义
- 2.2 命名空间使用
- 2.3嵌套命名空间
- 3. C++输入&输出
- 4. 缺省参数
- 4.1 缺省参数概念
- 4.2 缺省参数分类
- 5. 函数重载
- 5.1 函数重载概念
- 5.2 C++支持函数重载的原理--名字修饰(name Mangling)
- 6. 引用
- 6.1 引用概念
- 6.2 引用特性
- 6.3 常引用
- 6.4 使用场景
- 6.5 传值、传引用效率比较
- 6.6 引用和指针的区别
- 7. 内联函数
- 7.1 概念
- 7.2 特性
- 8. auto关键字(C++11)
- 8.1 auto简介
- 8.2 auto的使用细则
- 8.3auto不能推导的场景
- 9. 基于范围的for循环(C++11)
- 9.1 范围for的语法
- 9.2 范围for的使用条件
- 10. 指针空值nullptr(C++11)
- 10.1 C++98中的指针空值
前言
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C++学习有一定的帮助,本章节主要目标:
- 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
- 为后续类和对象学习打基础。
1. C++关键字(C++98)
C++总计63个关键字,C语言32个关键字
2. 命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{printf("%d\n", rand);return 0;
}
rand是一个库函数,所以不能使用rand做变量名称,会出现以下错误
C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
2.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。只要是能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间(这意味着命名空间可以嵌套):
namespace green//命名空间的名字
{//定义变量int rand = 10;//定义函数int Add(int left, int right){return left + right;}//定义类型struct Node{struct Node* next;int val;};//嵌套命名空间namespace green{int c;int d;int Sub(int left, int right){return left - right;}}
}
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。命名空间可以是不连续的。命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。因为不同命名空间的作用域不同,所以在不同命名空间内可以有相同名字的成员。
定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须通过域作用限定符::明确指出所用的名字属于哪个命名空间。
namespace green
{int a = 10;
}int a = 30;int main()
{int a = 20;printf("%d\n", a);printf("%d\n", ::a);printf("%d\n", green::a);return 0;
}
上面的代码在局部域、全局域、green命名空间域中分别定义了一个a变量并且赋了不同的值,在没指定作用域的时候,会根据局部优先的原则去访问局部变量;::a的左边什么也没有意味着去全局域中查找;green::a则指定了要到green这个域里面去查找。
2.2 命名空间使用
命名空间中成员该如何使用呢?比如:
namespace green
{int a = 0;int b = 1;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}
int main()
{printf("%d\n", a);return 0;
}
命名空间的使用有三种方式:
- 加命名空间名称及作用域限定符:
int main()
{printf("%d\n", green::a);return 0;
}
- 使用using将命名空间中某个成员引入:
using green::b;
int main()
{printf("%d\n", green::a);printf("%d\n", b);return 0;
}
- 使用using namespace 命名空间名称 引入
可以用using namespace green将命名空间展开,把命名空间中的成员提升到包含命名空间本身和using指示最近的作用域
namespace green
{int a = 10;int b = 5;int c = 100;
}int a = 30;using namespace green;int main()
{printf("%d\n", a);//a不明确,出现二义性printf("%d\n", ::a);//正确:访问全局的aprintf("%d\n", green::a);//正确:访问green中的aprintf("%d\n", b);//正确,去访问green中的bint c = 79;//当前局部变量的c隐藏了green::cc++;//当前局部的c设置成80return 0;
}
以上面的代码为例,通过using把wcy命名空间展开,这个过程相当于把wcy中的名字“添加”到全局作用域中,这使得程序可以直接访问green中的所有名字。
当命名空间被注入到它的外层作用域之后,很可能该命名空间中定义的名字会与其外层作用域中的成员冲突。例如在主函数中,green的成员a就与全局作用域中的a产生了冲突。这种冲突是允许存在的,但是要想使用冲突的名字,我们就必须明确指出名字的版本。main函数中所有未加限定的a都会产生二义性错误。
为了使用像a这样的名字,我们必须使用作用域运算符来明确指出所需的版本。我们使用::a来表示全局作用域中的a,而使用green::a来表示定义在green中的a。
因为main的作用域和命名空间的作用域不同,所以main内部的声明可以隐藏命名空间中的某些成员的名字。例如,局部变量c隐藏了命名空间的成员green::c。在main中使用c不存在二义性,他指的就是局部变量c。
2.3嵌套命名空间
namespace wcy
{int a = 10;namespace green1{int a = 900;//将外层作用域的a隐藏了int d = 1000;}//int b = d;//不正确:d是未声明的标识符int b = green1::d;//正确访问的是wcy1里面的dnamespace green2{int f = green1::d;//正确}
}int main()
{printf("%d\n", wcy::green2::f);printf("%d\n", wcy::green1::a);printf("%d\n", wcy::a);return 0;
}
3. C++输入&输出
新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{cout << "Hello world!!!" << endl;return 0;
}
说明:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。后面我们还有有一个章节更深入的学习IO流用法及原理。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支<iostream.h>格式,后续编译器已不支持,因此推荐使用 < iostream >+std的方式
#include <iostream>
using namespace std;
int main()
{int a;double b;char c;// 可以自动识别变量的类型cin >> a;cin >> b >> c;cout << a << endl;cout << b << " " << c << endl;return 0;
}
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
- 在日常练习中,建议直接using namespace std即可,这样就很方便。
- using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +using std::cout展开常用的库对象/类型等方式。
4. 缺省参数
4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
void Func(int a = 0)
{cout << a << endl;
}
int main()
{Func(); // 没有传参时,使用参数的默认值Func(10); // 传参时,使用指定的实参return 0;
}
4.2 缺省参数分类
- 全缺省参数
//全缺省
void Func(int a = 10, int b = 20, int c = 30)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}int main()
{Func();Func(1);Func(1, 2);Func(1, 2, 3);return 0;
}
- 半缺省参数
//半缺省(缺省值只能从右往左给,必须是连续给)
void Func(int a, int b =20, int c = 30)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}int main()
{//Func();//函数调用参数太少Func(1);Func(1, 2);Func(1, 2, 3);return 0;
}
- 半缺省参数必须从右往左依次来给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现(缺省参数只能出现在函数声明中)
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
5. 函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的**形参列表(参数个数 或 类型 或 类型顺序)**不同,常用来处理实现功能类似数据类型不同的问题。
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;
}
int main()
{Add(1, 2);Add(1.1, 2.2);return 0;
}
2、参数个数不同
void f()
{cout << "f()" << endl;
}
void f(int a)
{cout << "f(int a)" << endl;
}
int main()
{f();f(1);return 0;
}
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;
}
int main()
{f(10, 'a');f('a', 10);return 0;
}
4,有缺省参数的情况
void f()
{cout << "f()" << endl;
}
void f(int a = 0)
{cout << "f(int a)" << endl;
}
int main()
{//f();不传参数的时候调用存在二义性f(1);return 0;
}
上面代码中的两个fun函数根据函数重载的定义他俩是构成函数重载的,编译可以通过,因为第一个没有参数,第二个有一个整型参数,属于上面的参数个数不同的情况。但是fun函数存在一个问题:在无参调用的时候会产生歧义,因为对两个fun函数来说,都可以不传参。
注意:返回值是否相同与函数是否构成重载无关。
5.2 C++支持函数重载的原理–名字修饰(name Mangling)
6. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
- 类型& 引用变量名(对象名) = 引用实体
int main()
{int a = 0;int& b = a;//定义引用类型,b是a的引用int& c = a;return 0;
注意:引用类型必须和引用实体是同种类型的
让我们来验证下共同用一块内存空间
int main()
{int a = 0;int& b = a;int& c = a;cout << &a << endl;cout << &b << endl;cout << &c << endl;return 0;
}
6.2 引用特性
- 引用在定义时必须初始化
int main()
{int a = 10;int& d;//不能这样return 0;
}
- 一个变量可以有多个引用
int main()
{int a = 10;int& b = a;int& c = a;return 0;
}
- 引用一旦引用一个实体,再不能引用其他实体
int main()
{int a = 80;int num = 888;int& b = a;int& c = a;b = num;//这里是把num的值赋给a,并不是让b变成num的引用cout << "变量a的地址:" << &a << endl;cout << "引用b的地址:" << &b << endl;cout << "变量num的地址:" << &num << endl;return 0;
}
6.3 常引用
权限放大:
int main()
{const int a = 10;int b = a;//可以,值的拷贝int& b = a;//权限放大return 0;
}
a最初是一个常变量,意味着a一旦定义就不能再修改,而此时引用b出现了,它是a的一个别名,但是它没有加const修饰,意味着可以对b进行修改,这时就相当于权限的放大,这种情况是不允许的。正确的做法是,给引用b加上const进行修饰,即:const int& b = a;,此时属于权限的平移。
权限平移:
int main()
{const int a = 10;const int& b = a;//权限平移return 0;
}
权限缩小:
int main()
{// 权限可以缩小int c = 20;const int& d = c;const int& e = 10;
}
补充:
临时的中间变量具有常性。这条性质适用于所有的临时变量,不只是传值返回产生的临时变量具有常性,在类型转换(包括但不限于:强制类型转换、隐式类型转换、整型提升、截断)过程中,也会产生临时变量,并且这个临时变量也具有常性。为什么要提这个?
因为引用是针对同一块空间的操作,引用就是给同一块空间取别名,既然是同一块空间,就逃不了会涉及到权限变化问题,又因为临时变量不经过调试,我们是很难发现的它的存在,并且临时变量很特殊,具有常性,所以,我们需要特别注意哪些可能会产生临时变量的操作。下面举一些可能会产生临时变量的例子:
传值返回:
int Text()
{int a = 99;return a;
}
int main()
{//int& ret = Text();//函数返回会创建临时变量const int& ret = Text();//用引用接收必须要加const修饰return 0;
}
类型转换:
int main()
{double a = 3.14;//int& b = a;//错误的类型转换,产生临时变量const int& b = a;//正确return 0;
}
传参:
void Text1(int& y)
{cout << y << endl;
}void Text(const int& y)
{cout << y << endl;
}int main()
{//Text1(1 + 3);//错误Text(1 + 3);//正确return 0;
}
上面代码中的函数调用Text1(1 + 3);是错误的,因为1 + 3的结果会保存在一个临时变量里面,同时形参是一个引用,相当于要给这个临时变量取一个别名,但这个临时变量具有常性,而这里的引用只是一个普通引用,不是常引用,所以就会涉及权限的放大,导致函数调用出错。Text函数的形参是一个常引用,在调用的时候就不会出错。
6.4 使用场景
- 做参数
交换两个整型变量:
void Swap(int& num1, int& num2)
{int tmp = num1;num1 = num2;num2 = tmp;
}int main()
{int a = 10;int b = 11;cout << "a:" << a << " " << "b:" << b << endl;Swap(a, b);cout << "a:" << a << " " << "b:" << b << endl;return 0;
}
交换两个指针变量:
void Swap(int*& p1, int*& p2)
{int* tmp = p1;p1 = p2;p2 = tmp;
}int main()
{int a = 10;int b = 11;int* pa = &a;int* pb = &b;cout << "pa:" << pa << " " << "pb:" << pb << endl;Swap(pa, pb);cout << "pa:" << pa << " " << "pb:" << pb << endl;return 0;
}
- 做返回值
int add(int x, int y)
{int sum = x + y;return sum;
}int main()
{int a = 5;int b = 4;int ret = add(a, b);return 0;
}
上面代码中的add函数,实现了一个简单的两数求和,要将求和结果sum返回给调用它的地方,这里采用的是传值返回,由于sum是函数中的一个局部变量,存储在当前函数的栈帧中,随着函数调用结束栈帧销毁,sum也会随之灰飞烟灭。因此,对于这种传值返回,会生成一个临时的中间变量,用来存储返回值,在返回值比较小的情况下,这个临时的中间变量一般就是寄存器,下面通过调试来验证:
不仅函数中的普通局部变量在传值返回的时候会创建临时的中间变量,函数中static修饰的静态变量,虽然存储在内存中的静态区,不会随着函数调用结束而销毁,但是在传值返回的时候,同样会创建一个临时的中间变量,以下面的代码为例:
int Text()
{static int a = 10;return a;
}int main()
{int ret = Text();return 0;
}
尽管函数中的a是一个静态变量,没有存储在当前函数调用的栈帧中,但是在返回a的时候,还是创建了一个临时的中间变量来存储a。因此可以得出结论:
- 只要是传值返回,编译器都会生成一个临时的中间变量。
- 临时的中间变量具有常性。
传引用返回:
和传值返回不同,传引用返回不需要创建临时的中间变量,但前提是,在函数调用结束,函数栈帧销毁后,返回的变量任然存在。换句话说就是,返回的变量不能存储在函数调用所创建的栈帧中,即返回的变量,不能是普通的局部变量,而是存储在静态区的静态变量,或是在堆上动态申请得到的变量。
局部变量传引用返回存在的问题:
引用即别名,传引用返回,就是给一块空间取了一个别名,再把这个别名返回。一个局部变量的空间,是函数栈帧的一部分,这块空间会随着函数调用结束,函数栈帧的销毁而销毁,因此给这块空间取一个别名,再把这个别名返回给调用它的地方,这显然是有问题的,因为这块空间已经被释放了,归还给了操作系统。
int& add(int x, int y)
{int sum = x + y;return sum;
}
int main()
{int a = 5;int b = 4;int ret = add(a, b);cout << ret << endl;return 0;
}
还是上面这个求和代码,sum是一个局部变量,但是传引用返回,结果貌似没有什么问题,这是为什么呢?其实,sum标识的这块空间在函数调用结束,确确实实是归还给了操作系统,但是操作系统并没有将里面存储的内容清理,这就导致打印出来的结果貌似是正确的。可以对上面的代码稍作修改,继续验证:
int& add(int x, int y)
{int sum = x + y;return sum;
}int main()
{int a = 5;int b = 4;int& ret = add(a, b);cout << ret << endl;printf("hello\n");cout << ret << endl;return 0;
}
这一次验证,最重要的变化是从int ret = add(a, b);变成了int& ret = add(a, b);,可不要小瞧了这一个&,他让ret变成了引用,即ret从一个独立的变量,变成了一块空间的别名。原本调用add函数,返回sum所标识空间的一个别名,在把这块空间里的内容赋值给ret,而现在,ret也变成了sum所标识空间的别名,为什么要这样做?先看结果,两次打印ret的结果并不相同,第一次侥幸是正确的,因为sum标识的空间在归还给操作系统后,操作系统并没有对这块空间进行清理,接着调用了printf函数,由于函数调用会创建栈帧,sum标识的空间在此次创建的函数栈帧中被重新使用,这就导致里面存储的内容一定会发生改变,此时再去打印ret,结果就是错误的。假如这里的ret不是引用,是无法验证出这个错误的,因为此时ret有自己单独的空间,int ret = add(a, b);就是一次赋值操作,在第一次赋值后,ret就不会再变化,因此两次打印的结果可能侥幸都是正确的,所以需要让ret变成引用。
上面说了这么多就是想告诉大家,局部变量传引用返回,你的结果可能侥幸是正确的。所以对于局部变量,大家还是老老实实的用传值返回。
引用做返回值的优势:
- 减少拷贝,提高效率。
- 可以同时读取和修改返回值(重载[ ]就是利用这个优势)
6.5 传值、传引用效率比较
以值作为参数或返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常地下的,尤其是当参数或者返回值类型非常大时,效率就更低。
参数对比:
struct A
{int a[100000];
};void TestFunc1(A a)
{;
}void TestFunc2(A& a)
{;
}void TestFunc3(A* a)
{;
}//引用传参————可以提高效率(大对象或者深拷贝的类对象)
void TestRefAndValue()
{A a;// 以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)//就是单纯的调用一万次这个函数传一万次参TestFunc1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc2(a);//这里直接传的是变量名size_t end2 = clock();//以指针作为函数参数size_t begin3 = clock();for (int i = 0; i < 10000; i++){TestFunc3(&a);}size_t end3 = clock();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;
}int main()
{TestRefAndValue();return 0;
}
其中,A类型里面有一个四十万字节的数组,TestFunc1是值传递,TestFunc2是传引用,TestFunc3是传地址,分别把这三个函数调用一万次,通过结果可以看出,值传递花费的时间最长,并且也是最占用空间的,每次调用TestFunc1函数,都会重新创建一个四十万字节的A类型的变量,来存储实参,而传引用,形参只是给实参所标识的内存空间取了一个别名,并没有创建新的空间,传地址,只会创建一块空间来存储实参的地址,这块空间在32位机下是4字节,在64位机下是8字节。
返回值对比:
struct A
{int a[100000];
};
A a;//全局的,函数栈帧销毁后还在
// 值返回
A TestFunc1()
{return a;
}
// 引用返回
A& TestFunc2()
{return a;
}
void TestReturnByRefOrValue()
{// 以值作为函数的返回值类型size_t begin1 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc1();//就让他返回不接收size_t end1 = clock();// 以引用作为函数的返回值类型size_t begin2 = clock();for (size_t i = 0; i < 100000; ++i)TestFunc2();size_t end2 = clock();// 计算两个函数运算完成之后的时间cout << "TestFunc1 time:" << end1 - begin1 << endl;cout << "TestFunc2 time:" << end2 - begin2 << endl;
}int main()
{TestReturnByRefOrValue();return 0;
}
值返回每次都要创建临时的中间变量,这就导致效率下降和空间上的浪费。
6.6 引用和指针的区别
- 引用在概念上定义一个变量的别名,指针存储一个变量的地址。
- 引用在定义时必须初化,指针没有要求。
- 引用在初始化时引用一个一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL空指针。
- 在sizeof中的含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位机下占四个字节,64位机下占八个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没多级引用。
- 访问实体方式不同。指针显式解引用,引用编译器自己做处理。
- 引用比指针使用起来相对更安全。
7. 内联函数
普通的函数在调用的时候会开辟函数栈帧,会产生一定量的消耗,在C语言中可以用宏函数来解决这个问题,但是宏存在以下缺陷:复杂、容易出错、可读性差、不能调试。为此,C++中引入了内联函数这种方法。
7.1 概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,所以内联函数可以提高程序的运行效率。
//普通函数
int Add(int x, int y)//这里的Add是一个普通函数
{return x + y ;
}int main()
{int ret = 0;ret = Add(3, 5);cout << ret << endl;return 0;
}
//内联函数
inline int Add(int x, int y)
{return x + y ;
}int main()
{int ret = 0;ret = Add(3, 5);cout << ret << endl;return 0;
}
7.2 特性
- inline是一种以时间换空间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大,优点:少了调用开销,提高程序运行效率。
- inline对编译器而言只是建议,不同的编译器关于inline的实现机制可能不同,一般建议:将函数规模小的(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline建议函数声明和定义不能分离,因为内联函数在预处理阶段就直接展开,因此内联函数不会进符号表,因此如果声明和定义分离,头文件只有声明,在预处理阶段,头文件展开,只知道该函数是一个内联函数,没有对应函数的定义,因此就无法完成替换,那就只能等通过call在链接阶段去找该函数,但是它是内联函数,没有进符号表,所以链接阶段就会报错。
为什么是函数规模小?
假设一个函数经过编译,得到五十条汇编指令。普通情况下,调用此函数只需要一条call指令,调用10000此也就10000条call指令,但是如果把这个函数设置成内联函数,指令的数量就会大大增加,因为内联函数完成的是替换,把所有调用它的地方,都用函数体去替换,这也就意味着,原来1条call指令就能完成的任务,现在替换后就变成了50条指令,假如还是调用了10000次该函数,那就从10000条call指令,变成了500000条指令,其实这就是代码膨胀。
inline int Add(int x, int y)
{cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;cout << "xxxxxxxxxxxx" << endl;return x + y ;
}int main()
{int ret = 0;ret = Add(3, 5);cout << ret << endl;return 0;
}
对于上面函数体比较长的函数,即使我们人为规定了它是内联,但最终还是通过call指令去调用函数。
为什么是被频繁调用?
因为普通函数在调用的时候会创建函数栈帧,若频繁调用就会频繁的创建栈帧,增加消耗。宏和内联,就是为了解决开销问题。如果调用的次数不多,开辟一点栈帧是无所谓的。
8. auto关键字(C++11)
8.1 auto简介
C++11中规定:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。简单来说,auto会根据表达式自动推导类型。
int main()
{int a = 0;auto b = a;//自动推导出b的类型是intauto c = 1.11 + 1;//自动推导出c的类型是doublecout << typeid(b).name() << endl;//typeid可用来查看变量类型cout << typeid(c).name() << endl;return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式,来推导auto的实际类型。因此,auto并非是一种“类型”的声明,而是一个类型声明的“占位符”,编译器在编译阶段会将auto替换为变量实际的类型。
int main()
{auto a;//错误,必须要初始化return 0;
}
8.2 auto的使用细则
auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和aauto*没有任何区别,但是auto声明引用类型时,必须要加&,如下,如果c不加&的话,就是x的一份拷贝。
int main()
{int x = 10;auto a = &x;//根据右边推出,a是一个指针类型auto* b = &x;//右边必须是一个地址,因为前面加了*auto& c = x;//引用必须要加&
}
在同一行定义多个变量
当在同一行声明多个变量的时候,这些变量必须是相同的类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{auto a = 10, b = 30;auto c = 60, d = 1.1;//该行编译失败,c和d的初始化类型不同
}
8.3auto不能推导的场景
- auto不能作为函数的参数
//错误,编译器无法对x的实际类型进行推导
void Text(auto x)
{}
- auto不能直接用来声明数组
void Text()
{//auto arr[] = { 1, 2, 3 };//错误写法,请勿模仿int arr[] = {1, 2, 3}//这才是正确写法
}
小Tips:auto在实际中常被用在:基于范围的for循环中、还有lambda表达式中、其次就是一些非常非常长的类型,也会用auto进行替换。
9. 基于范围的for循环(C++11)
9.1 范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)array[i] *= 2;for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)cout << *p << endl;
}
C++98中遍历一个数组:
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还容易犯错误。因此C++11中引入了基于范围for循环。for循环后的括号由冒号“ : ”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{int array[] = { 1, 2, 3, 4, 5 };for(auto& e : array)//加引用可以对后面的值修改e *= 2;for(auto e : array)cout << e << " ";return 0;
}
9.2 范围for的使用条件
- for循环迭代的范围必须是确定的
- 迭代的对象要实现++和==的操作
对数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end方法,begin和end就是for循环的迭代范围。范围for本质上是迭代器,支持迭代器就支持范围for。
void Text(int arr[])//arr本质上只是一个地址,没有范围
{for (auto a : arr)//错误{cout << a << endl;}
}
10. 指针空值nullptr(C++11)
10.1 C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{int* p1 = NULL;int* p2 = 0;// ……
}
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。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
💘不知不觉,【初阶C++】入门(超详解)学习告一段落。通读全文的你肯定收获满满,让我们继续为C++学习共同奋进!!!
相关文章:

【初阶C++】入门(超详解)
C入门 前言1. C关键字(C98)2. 命名空间2.1 命名空间定义2.2 命名空间使用2.3嵌套命名空间 3. C输入&输出4. 缺省参数4.1 缺省参数概念4.2 缺省参数分类 5. 函数重载5.1 函数重载概念5.2 C支持函数重载的原理--名字修饰(name Mangling) 6. 引用6.1 引用概念6.2 引用特性6.3 …...

Java正则表达式的使用
标题:Java正则表达式的使用 介绍: 正则表达式是一种强大的文本匹配模式和搜索工具。在Java中,通过使用正则表达式,我们可以快速有效地进行字符串的匹配、查找和替换。本文将介绍Java正则表达式的基本使用方法,并提供相…...

Collecting Application Engine Performance Data 收集应用程序引擎性能数据
You can collect performance data of any specific SQL action of an Application Engine program to address any performance issue. 您可以收集应用程序引擎程序的任何特定SQL操作的性能数据,以解决任何性能问题。 You can collect performance data of the S…...

C Primer Plus阅读--章节16
C Primer Plus阅读–章节16 翻译程序的第一步 预处理之前,编译器必须对该程序进行一些翻译处理。 首先,编译器将源代码中出现的字符映射到源字符集。第二,编译器定位每个反斜杠后面跟着换行符的实力,并删除他们。物理行的合并。…...

直接插入排序与希尔排序
目录 前言 插入排序 直接插入排序 时空复杂度 直接插入排序的特性 希尔排序(缩小增量排序) 预排序 顺序排序 多组并排 小总结 直接插入排序 时空复杂度 希尔排序的特性 前言 字可能有点多,但是真的理解起来真的没那么难&#…...

敏捷:应对软件定义汽车时代的开发模式变革
随着软件定义汽车典型应用场景的落地,汽车从交通工具转向智能移动终端的趋势愈发明显。几十年前,一台好车的定义主要取决于高性能的底盘操稳与动力系统;几年前,一台好车的定义主要取决于智能化系统与智能交互能否满足终端用户的用…...

做题笔记:SQL Sever 方式做牛客SQL的题目--查询每天刷题通过数最多的前二名用户
----查询每天刷题通过数最多的前二名用户id和刷题数 现有牛客刷题表questions_pass_record,请查询每天刷题通过数最多的前二名用户id和刷题数,输出按照日期升序排序,查询返回结果名称和顺序为: date|user_id|pass_count 表单创建…...

Vue3 用 Proxy API 替代 defineProperty API 的那些事
一、Object.defineProperty 定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象 1.1 为什么能实现响应式 通过defineProperty 两个属性,get及set get 属性的 gett…...

成都工业学院Web技术基础(WEB)实验五:CSS3动画制作
写在前面 1、基于2022级计算机大类实验指导书 2、代码仅提供参考,前端变化比较大,按照要求,只能做到像,不能做到一模一样 3、图片和文字仅为示例,需要自行替换 4、如果代码不满足你的要求,请寻求其他的…...

【Docker】学习笔记(三)三剑客之 docker-compose文件书写项目多服务容器运行
简介 引言(需求) 为了完成一个完整项目势必用到N多个容器配合完成项目中的业务开发,一旦引入N多个容器,N个容器之间就会形成某种依赖,也就意味着某个容器的运行需要其他容器优先启动之后才能正常运行; 容…...

node.js基础
node.js基础 🍓什么是node.js🍓node.js模块🍒🍒 内置模块🍅🍅🍅fs模块🍅🍅🍅path模块🍅🍅🍅http模块 🍒&#…...

fastapi实现websocket在线聊天
最近要实现一个在线聊天功能,基于fastapi的websocket实现了这个功能。下面介绍一下遇到的技术问题 1.问题难点 在线上环境部署时,一般是多进程的方式进行部署启动fastapi服务,而每个启动的进程都有自己的独立存储空间。导致存储的连接对象分…...

LLM推理部署(六):TogetherAI推出世界上LLM最快推理引擎,性能超过vLLM和TGI三倍
LLM能有多快?答案在于LLM推理的最新突破。 TogetherAI声称,他们在CUDA上构建了世界上最快的LLM推理引擎,该引擎运行在NVIDIA Tensor Core GPU上。Together推理引擎可以支持100多个开源大模型,比如Llama-2,并在Llama-2–…...

Unity | 渡鸦避难所-2 | 搭建场景并添加碰撞器
1 规范项目结构 上期中在导入一系列的商店资源包后,Assets 目录已经变的混乱不堪 开发过程中,随着资源不断更新,遵循一定的项目结构和设计规范是非常必要的。这可以增加项目的可读性、维护性、扩展性以及提高团队协作效率 这里先做下简单的…...

展望2024年供应链安全
2023年是开展供应链安全,尤其是开源治理如火如荼的一年,开源治理是供应链安全最重要的一个方面,所以我们从开源治理谈起。我们先回顾一下2023的开源治理情况。我们从信通院《2023年中国企业开源治理全景观察》发布的信息。信通院调研了来自七…...

React 列表页实现
一、介绍 列表页是常用的功能,从后端获取列表数据,刷新到页面上。开发列表页需要考虑以下技术要点:1.如何翻页;2.如何进行内容搜索;3.何时进行页面刷新。 二、使用教程 1.user-service 根据用户id获取用户列表,返回…...

【程序人生】还记得当初自己为什么选择计算机?
✏️ 初识计算机: 还记得人生中第一次接触计算机编程是在高中,第一门编程语言是Python(很可惜由于条件限制的原因,当时没能坚持学下去......现在想来有点后悔,没能坚持,唉......)。但是…...

9-tornado-Template优化方法、个人信息案例、tornado中ORM的使用(peewee的使用、peewee_async)、WTForms的使用
在很多情况下,前端模板中在很多页面有都重复的内容可以使用,比如页头、页尾、甚至中间的内容都有可能重复。这时,为了提高开发效率,我们就可以考虑在共同的部分提取出来, 主要方法有如下: 1. 模板继承 2. U…...

IDEA中.java .class .jar的含义与联系
当使用IntelliJ IDEA这样的集成开发环境进行Java编程时,通常涉及.java源代码文件、.class编译后的字节码文件以及.jar可执行的Java存档文件。 1. .java 文件: 1.这些文件包含了Java源代码,以文本形式编写。它们通常位于项目中的源代码目录中…...

北斗三号短报文森林消防应急通信及天通野外图传综合方案
森林火灾突发性强、破坏性大、危险性高,是全球发生最频繁、处置最困难、危害最严重的自然灾害之一,是生态文明建设成果和森林资源安全的最大威胁,甚至可能引发生态灾难和社会危机。我国总体上是一个缺林少绿、生态脆弱的国家,是一…...

js Array.every()的使用
2023.12.13今天我学习了如何使用Array.every()的使用,这个方法是用于检测数组中所有存在的元素。 比如我们需要判断这个数组里面的全部元素是否都包含张三,可以这样写: let demo [{id: 1, name: 张三}, {id: 2, name: 张三五}, {id: 3, name…...

前端编码中快速填充内容--乱数假文
写前端页面的时候,如果要快速插入图片,可以使用 https://picsum.photos/ 详见笔者这篇博文: 工具网站:随机生成图片的网站-CSDN博客 可是,如果要快速填充文字内容该怎么做呢? 以前,我们都是…...

数据结构二维数组计算题,以行为主?以列为主?
1.假设以行序为主序存储二维数组Aarray[1..100,1..100],设每个数据元素占2个存储单元,基地址为10,则LOC[5,5]( )。 A.808 B.818 C.1010 D&…...

springboot(ssm电影院订票信息管理系统 影院购票系统Java系统
springboot(ssm电影院订票信息管理系统 影院购票系统Java系统 开发语言:Java 框架:ssm/springboot vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库:mysql 5.7(或8.0࿰…...

AI 问答-供应链管理-相关概念:SCM、SRM、MDM、DMS、ERP、OBS、CRM、WMS...
一、供应链管理是什么 供应链管理:理解供应链管理_snowli的博客-CSDN博客 二、SCM 供应链管理 SCM全称为“Supply Chain Management”,即供应链管理。 SCM是企业管理范畴中一个非常重要的概念,指的是企业与供应商、生产商、分销商等各方之…...

初学vue3与ts:vue3选项式api获取当前路由地址
vue2的获取方法 this.$route.pathvue3选项式api获取方法 import { useRouter } from vue-router; const router useRouter(); console.log(router) console.log(router.currentRoute.value.path)...

2023最新大模型实验室解决方案
人工智能是引领未来的新兴战略性技术,是驱动新一轮科技革命和产业变革的重要力量。近年来,人工智能相关技术持续演进,产业化和商业化进程不断提速,正在加快与千行百业深度融合。 大模型实验室架构图 大模型实验室建设内容 一、课…...

leetcode707.设计链表
题目描述 你可以选择使用单链表或者双链表,设计并实现自己的链表。 单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。 如果是双向链表,则还需要属性 prev 以指示链表中的…...

【K8s】Kubernetes CRD 介绍(控制器)
文章目录 CRD 概述1. 操作CRD1.1 创建 CRD1.2 操作 CRD 2. 其他笔记2.1 Kubectl 发现机制2.2 校验 CR2.3 简称和属性 3. 架构设计3.1 控制器概览 参考 CRD 概述 CR(Custom Resource)其实就是在 Kubernetes 中定义一个自己的资源类型,是一个具…...

Python 小程序之PDF文档加解密
PDF文档的加密和解密 文章目录 PDF文档的加密和解密前言一、总体构思二、使用到的库三、PDF文档的加密1.用户输入模块2.打开并读取文档数据3.遍历保存数据到新文档4.新文档进行加密5.新文档命名生成路径6.保存新加密的文档 四、PDF文档的解密1.用户输入模块2.前提准备2.文件解密…...