首页 > 基础资料 博客日记
【初阶C++】入门(超详解)
2024-02-18 21:56:30基础资料围观310次
C++入门
前言
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);//正确:访问全局的a
printf("%d\n", green::a);//正确:访问green中的a
printf("%d\n", b);//正确,去访问green中的b
int c = 79;//当前局部变量的c隐藏了green::c
c++;//当前局部的c设置成80
return 0;
}
以上面的代码为例,通过using把green命名空间展开,这个过程相当于把green中的名字“添加”到全局作用域中,这使得程序可以直接访问green中的所有名字。
当命名空间被注入到它的外层作用域之后,很可能该命名空间中定义的名字会与其外层作用域中的成员冲突。例如在主函数中,green的成员a就与全局作用域中的a产生了冲突。这种冲突是允许存在的,但是要想使用冲突的名字,我们就必须明确指出名字的版本。main函数中所有未加限定的a都会产生二义性错误。
为了使用像a这样的名字,我们必须使用作用域运算符来明确指出所需的版本。我们使用::a来表示全局作用域中的a,而使用green::a来表示定义在green中的a。
因为main的作用域和命名空间的作用域不同,所以main内部的声明可以隐藏命名空间中的某些成员的名字。例如,局部变量c隐藏了命名空间的成员green::c。在main中使用c不存在二义性,他指的就是局部变量c。
2.3嵌套命名空间
namespace green
{
int a = 10;
namespace green1
{
int a = 900;//将外层作用域的a隐藏了
int d = 1000;
}
//int b = d;//不正确:d是未声明的标识符
int b = green1::d;//正确访问的是green1里面的d
namespace green2
{
int f = green1::d;//正确
}
}
int main()
{
printf("%d\n", green::green2::f);
printf("%d\n", green::green1::a);
printf("%d\n", green::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;
}
补充:
临时的中间变量具有常性。这条性质适用于所有的临时变量,不只是传值返回产生的临时变量具有常性,在类型转换(包括但不限于:强制类型转换、隐式类型转换、整型提升、截断)过程中,也会产生临时变量,并且这个临时变量也具有常性。为什么要提这个?
因为引用是针对同一块空间的操作,引用就是给同一块空间取别名,既然是同一块空间,就逃不了会涉及到权限变化问题,又因为临时变量不经过调试,我们是很难发现的它的存在,并且临时变量很特殊,具有常性,所以,我们需要特别注意哪些可能会产生临时变量的操作。下面举一些可能会产生临时变量的例子:
传值返回:
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的类型是int
auto c = 1.11 + 1;//自动推导出c的类型是double
cout << 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++学习共同奋进!!!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: