2-2 auto(c++11)、头文件防卫、引用(底层实现)、常量(const、 constexpr(c++11)、nullptr(c++11))

目录

auto(c++11)

防卫式声明

引用

C以及C++在const的区别,以及const在C++的增强

nullptr与constexpr(c++11)


auto(c++11)

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量。

int a =10 ;  //拥有自动生命期
auto int b = 20 ;//拥有自动生命期
static int c = 30 ;//延长了生命期

C++11中,auto有了全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

通俗地讲,auto关键字是可以自动推导变量类型的。

auto的自动类型推断发生在编译期,所以使用auto并不会造成程序运行时效率的降低。而是否会造成编译期的时间消耗,我认为是不会的,在未使用auto时,编译器也需要得知右操作数的类型,再与左操作数的类型进行比较,检查是否可以发生相应的转化,是否需要进行隐式类型转换。

需要注意的是,auto不是一个类型的“声明”,而是一个“占位符”,编译器在编译期会将auto替换为变量实际的类型。使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。它自动推导变量类型是根据“=”右侧的变量类型决定的。下面是一段例子:

#include <iostream>
#include <typeinfo>
using namespace std;
 
int TestAuto()
{
	return 10;
}
 
int main()
{
	int a = 10;
	auto b = a;//由a是int,可以推导出b的类型是int
	auto c = 'a';//由‘a’推导出c的类型是char
	auto d = TestAuto();
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
 
    auto e;//这条语句编译不通过,使用auto定义变量时,必须对其进行初始化
	system("pause");
	return 0;
}
//typeid(b).name()是打印类型名称的函数

1. auto与指针和引用结合起来使用

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main()
{
  int x = 10;
  auto a = &x;
  auto* b = &x;
  auto& c = x;
  cout << typeid(a).name() << endl;
  cout << typeid(b).name() << endl;
  cout << typeid(c).name() << endl;
  *a = 20;
  *b = 30;
  c = 40;
  return 0;
}

2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量。

#include <iostream>
using namespace std;
 
void TestAuto()
{
	auto a = 1, b = 2;
	//auto c = 3, d = 4.0;//错误
	auto c = 3, d = 4;//正确
	cout << c << endl;
	cout << d << endl;
}
 
int main()
{
	TestAuto();
	
	system("pause");
	return 0;
}

不可以使用的场景:

1. auto不能作为函数的参数

 参数要被编译成指令,但是开辟空间时候需要知道空间大小,auto做参数不知道多大,那么栈帧也不知道开多大。

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2. auto不能直接用来声明数组

void TestAuto()
{
	int a[] = { 1, 2, 3 };
	auto b[3] = a;//auto类型不能出现在顶级数组类型中
}

因为数组也涉及大小,在下面的例子中,a的类型严格来说是 int [3],所以b的大小也不确定。

3. 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4. auto在实际中最常见的优势用法是C++11提供的新式for循环,还有lambda表达式等进行配合使用。

#include <iostream>
using namespace std;
 
int main()
{
	int array[] = { 1, 2, 3, 4, 5 };
	for (auto e : array)//依次取array里面的元素读给e
		cout << e << endl;
	
	system("pause");
	return 0;
}


5. auto不能定义类的非静态成员变量

 

6. 实例化模板时不能使用auto作为模板参数

//在定义模板函数时,用于声明依赖模板参数的变量类型。
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x*y;
    std::cout << v;
}
//若不使用auto变量来声明v,那这个函数就难定义啦,不到编译的时候,谁知道x*y的真正类型是什么呢?
//模板函数依赖于模板参数的返回值
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(x*y)
{
    return x*y;
}

当模板函数的返回值依赖于模板的参数时,我们依旧无法在编译代码前确定模板参数的类型,故也无从知道返回值的类型,这时我们可以使用auto。格式如上所示。

decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,其目的也是解决泛型编程中有些类型由模板参数决定,而难以表示它的问题。
auto在这里的作用也称为返回值占位,它只是为函数返回值占了一个位置,真正的返回值是后面的decltype(_Tx*_Ty)。为何要将返回值后置呢?如果没有后置,则函数声明时为:

decltype(x*y)multiply(_Tx x, _Ty y)
而此时x,y还没声明呢,编译无法通过。

  • 如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义。

    const int a1 = 10;
    auto  b1= a1; //b1的类型为int而非const int(去除const)
    const auto c1 = a1;//此时c1的类型为const int
    b1 = 100;//合法
    c1 = 100;//非法
  • 如果auto关键字带上&号,则不去除const语意。

    const int a2 = 10;
    auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int
    b2 = 10; //非法
    这是因为如何去掉了const,则b2为a2的非const引用,通过b2可以改变a2的值,则显然是不合理的。
  • 初始化表达式为数组时,auto关键字推导类型为指针。

    int a3[3] = { 1, 2, 3 };
    auto b3 = a3;
    cout << typeid(b3).name() << endl;

    程序将输出

    int *

  • 若表达式为数组且auto带上&,则推导类型为数组类型。

    int a7[3] = { 1, 2, 3 };
    auto & b7 = a7;
    cout << typeid(b7).name() << endl;

    程序输出

    int [3]

  • 函数或者模板参数不能被声明为auto

    void func(auto a)  //错误
    {
    //... 
    }
  • 时刻要注意auto并不是一个真正的类型。
    auto仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid。

    cout << sizeof(auto) << endl;//错误
    cout << typeid(auto).name() << endl;//错误

 

防卫式声明

我们需要知道,在预编译阶段,编译器会把.h文件展开,防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。

#ifndef 依赖于宏定义名,当宏已经定义时,#endif之前的代码就会被忽略,但是这里需要注意宏命名重名的问题;

#pragma once 只能保证同一个文件不会被编译多次,但是当两个不同的文件内容相同时,仍然会出错。而且这是微软提供的编译器命令,当代码需要跨平台时,需要使用宏定义方式。

1. 宏定义

1

2

3

4

#ifndef __FILENAME__

#define __FILENAME__

//...

#endif

2. 编译器指令

1

#pragma once

  1. 编译器将处理掉所有注释,以空格代替;
  2. 删除#define,展开所有宏定义;
  3. 处理条件编译指令#if、#ifdef、#elif、#else、#endif;
  4. 处理#include,展开被包含的头文件(直接将头文件复制进文件)
  5. 保留编译器需要使用的#progma指令等等。

编译器还会做很多其他事情,但是从第四条可以看出,会将头文件中写的代码直接复制进文件。那么可以想象,如果有多份头文件均不进行防卫式声明,均包含了类似<iostream>这种内容很多的头文件,经过预处理以后的文件,即便自己只写了一行代码cout,它包含的代码量将是何其的庞大。这还不是最关键的问题,关键是头文件中定义了一个变量,那么多次包含该头文件之后,就会产生重复定义的问题,那么防卫式声明其实防止重复声明与定义。

 

引用

初学c++中的“引用”这一概念的时候,很多人都是懵的,大家大概都会产生这样的疑问?
什么是引用?
引用占用内存吗?

于是,为了验证你的猜想,你可能会写出下面这样的代码来验证:

#include<iostream>
using namespace std;
int main()
{
	int  a = 1;
	int&  b = a;
	cout << "a:address->" << &a << endl;
	cout << "b:address->" << &b << endl;
	
	getchar();
	return 0;
}

运行结果:
a:address->0031FD54
b:address->0031FD54

我们会发现,引用b的地址和变量a的地址一样。于是,有人猜想是不是说变量a和引用b本身就是一个东西。所以同样的,引用本身所占内存就是变量a的内存。

**首先对于这个说法,肯定是不正确的。**至于为什么不正确,我们接下来会以底层原理为大家解释。

##什么是引用

为了看看引用的底层究竟是怎样实现的,我决定写一个简短的代码,然后反汇编(vs编译环境在调试模式下,右键鼠标菜单->反汇编)看一看。

#include<iostream>
using namespace std;
int main()
{
   int x=1;
   int &b=x;
   return 0;
}
9:       int x = 1; 	//源代码 
00401048   mov         dword ptr [ebp-4],1	//反汇编代码  
10:      int &b = x; 	//源代码
0040104F   lea         eax,[ebp-4]  		//反汇编代码
00401052   mov         dword ptr [ebp-8],eax//反汇编代码

在这里解释下这三行反汇编代码:
mov dword ptr [ebp-4],1 //把1赋值给ebp(栈底指针)-4的地址
lea eax,[ebp-4] //把ebp-4的地址赋值给寄存器eax
mov dword ptr [ebp-8],eax //把寄存器eax里的值赋值给ebp-8的这块地址
上述三行代码的作用就是将1赋值给x,然后将x的地址赋值给了引用b。
而在内存中,它是这样的:
这里写图片描述

注意:因为栈在内存中是由高地址向低地址增长的
通过底层的分析,我们不难理解引用的本质就是所引用对象的地址。

建议:有兴趣的同学可以了解一下常见的汇编指令,对于了解代码底层原理有很大的帮助。
##引用占用内存吗
通过上面的分析,我们得出了引用本身存放的是引用对象的地址,通俗点理解就是引用就是通过指针来实现的,所以,应用所占的内存大小就是指针的大小。
##引用的地址
在最开始,我们写过一段代码来测试引用的地址,发现引用的地址和变量的地址是一样的。但是,在后面对引用的底层分析后发现,它本身又存放的是变量的地址,即引用的值是地址,那么这不是很冲突吗?

事实上, b的地址我们没法通过&b获得,因为编译器会将&b解释为:&(*b) =&x ,所以&b将得到&x。也验证了对所有的b的操作,和对x的操作等同。

那么问题来了,我们如何才能获得引用的地址呢?
我们看下面这段代码:

#include<stdio.h>
#include <iostream>  
using namespace std;
int main()  
{  
   int x = 1;  
   int y = 2;  
   int &b = x;  
   printf("&x=%x,&y=%x,&b=%x,b=%x\n",&x,&y,&y-1,*(&y-1));  
   return 0;
 }   

输出:
&x=12ff7c,&y=12ff78,&b=12ff74,b=12ff7c

不知道看到这里大家明白了没有,引用b的地址我们可以间接通过&y-1来得到b的地址,从而得到b的值:*(&y-1) 从结果可以知道,b的值即x的地址,从而可以知道,从地层实现来看,引用变量的确存放的是被引用对象的地址,只不过,对于高级程序员来说是透明的,编译器 屏蔽了引用和指针的差别。

说明:大家在实践的时候,这里的x和y的地址不一定是连续的,因为这跟地址分配有关。
我们这里的研究只是为了通过这段代码为大家更好地解释引用和指针的区别。

如果还不明白,我们继续看这段代码变量的内存布局图:

这里写图片描述
最后要注意一点的是:引用就是引用,指针就是指针,引用不代表指针,一定不要混淆。

 

C以及C++在const的区别,以及const在C++的增强

C语言中const是一个伪常量,可以通过指针间接赋值改变,在C++中const得到加强,定义的是真常量。

const int a = 3;

const int b = 4;

int array[a + b] = { 0 };

以上语句在C语言报错,在C++中通过,即可说明真伪常量的区别。

定义:const type name=value;注意:初始化和定义一起

C语言通过指针改变const值:

void test5()
{
	const int a ;
	//int const b; //const int , int const 是等价的
 
	int *p = (int*)&a;
	*p = 20;//C语言中可以通过指针的间接赋值改变const变量 
 
	printf("a = %d\n", a);
}

在C++中指针无法这样的原因是,C++没有像C语言一样给const常量分配内存单元,const常量被放在一个只读的符号表中

例如定义一些const常量:const int a=10;   int const b=20;

C++中使用如上指针变量间接赋值时,指针p指向的是编译器在栈中找的一个临时存储单元(即将临时空间的地址赋给p),因此在C++中,执行上述代码后,a的值没有改变。

 

不管是在C还是C++中int const b; const int b 是等价的,然而const int *a和int *const a却是有区别的。

const int *a  —— 一个指向常整形数的指针(所指向的内存数据不能被修改,但是本⾝(即地址)可以修改)

int *const a —— 常指针(指针变量不能被修改,但是它所指向内存空间可以被修改)

const int* const a —— 一个指向常整形的常指针(指针和它所指向的内存空间,均不能被修改)

用几个代码区分他们:

struct student
{
	int id;
	char name[64];
};
 
void change_stu(struct student *s)
{
	s ->id = 10;//常规指针变量
}
 
void change_stu2(const struct student *s)
{
	s->id=10; //此时s所指向的区域是一个常量 不能够被修改,报错
	struct student s2;//可以创建一个新的变量取其地址赋给s,编译通过,说明s本身是可以改变的
	s = &s2;
	cout << s << endl;
}
 
void change_stu3(struct student *const s)
{
	s->id = 10;//编译通过,s是一个常量指针,本身不可变,但指向的区域是可变的
	struct student s2;//再次报错,因为s是一个常量指针
	s = &s2;  
}
 
void change_stu4(const struct student *const s)
{
	s->id = 10;
	struct student s2;
	s = &s2;  //以上都是错的,s是一个指向常量的常量指针。
}

再来谈谈,const和#define的区别吧

1.编译阶段不一样:const是编译器处理,define是预处理器

2.const划分作用域,define是全局的

建议使用const

nullptr与constexpr(c++11)

一.nullptr

NULL是一个宏定义,在c和c++中的定义不同: 在传统的C头文件(stddef.h)中,可以看到如下代码:

//C语言中NULL定义
#define NULL (void*)0                //c语言中NULL为void类型的指针,但允许将NULL定义为0

//c++中NULL的定义
#ifndef NULL
#ifdef _cpluscplus                       //用于判定是c++类型还是c类型,详情看上一篇blog
#define NULL 0                         //c++中将NULL定义为整数0
#else
#define NULL ((void*)0)             //c语言中NULL为void类型的指针
#endif
#endif

nullptr是一个字面值常量,类型为std::nullptr_t,空指针常数可以转换为任意类型的指针类型。

对于函数重载:若c++中 (void *)支持任意类型转换,函数重载时将出现问题下列代码中fun(NULL)将不能判断调用哪个函数

void fun(int i){cout<<"1";};
void fun(char *p){cout<<"2";};
int main()
{
  fun(NULL);  //输出1,c++中NULL为整数0
  fun(nullptr);//输出2,nullptr 为空指针常量。是指针类型
}

问:为什么C中(void*)0是空指针常量,而C++中不是?

答:因为C语言中任何类型的指针都可以(隐式地)转换为void*型,反过来也行,而C++中void*型不能隐式地转换为别的类型指针(例如:int*p = (void*)0;使用C++编译器编译会报错)


#include <iostream>

void foo(char * c){}
void foo(int n){}

int main()
{
    foo(0);
    // foo(NULL); // 编译无法通过
    foo(nullptr);
    return 0;
}

foo(NULL)无法编译通过,因为编译器不知道NULL隐式转换为哪个类型的参数来调用。

所以,当需要使用 NULL 时候,请养成直接使用 nullptr 的习惯。

 

为了考虑兼容性,C++11并没有消除常量0的二义性,而是给出了全新的nullptr,表示空值指针。C++11为什么不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏, 而且不同的编译器厂商对于NULL的实现可能不太相同,而且直接扩展NULL,可能会影响以前旧的程序。因此:为了避免混淆,C++11提供了nullptr,即: nullptr代表- 个指针空值常量。nullptr是有类型的, 其类型为nullptr_t,仅仅可以被隐式转化为指针类型,nullptr _t被定义在头文件中:

typedef decltype(nullptr) nullptr_t;

需要注意的是:

1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

2.在C++11中,sizeof(nullptr) 与sizeof(void*)0)所占的字节数相同。

3.为了提高代码的健壮性,建议在后续表示指针空值时,最好使用nullptr。

这里还有常见的一类题:

请区分:NULL、0、'\0'、"\0"?

NULL:是被定义出来的一个宏,它不是关键字,值为0;

0:是整形的0,值为0;

'\0':是字符0,值为0;

"\0":是字符串;

计算:

#include <iostream>
using namespace std;
int main()
{
    char str[] = "\0";
    cout << strlen(str)<< endl;//0字节
    cout << sizeof(str) << endl;//2个字节
    system("pause");
    return 0;
}
 

其中strlen(str)大小为0,因为转义字符\+0,就是字符串中的结束标志"\0",只要遇到"\0",它就自动停下来了,所以strlen(str)大小为0;sizeof(str)大小为2个字节,转义字符\+0是一个字符,还有字符串结束标志的那个"\0",加起来就是2个字节。

#include <iostream>
using namespace std;
 
int main()
{
    char str[] = "\\0";
    cout << strlen(str)<< endl;//2个字节
    cout << sizeof(str) << endl;//3个字节
    system("pause");
    return 0;
}

这个程序里面strlen(str)大小为2个字节,转义字符\+\转义为\,后面还有个数字0,总共是2个字节;sizeof(str)大小为3个字节。

https://www.cnblogs.com/henryliublog/p/9121559.html

https://www.cnblogs.com/nothx/p/8523191.html

 

二.constexpr

C++11新标准规定,允许将变量或函数声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化

1.constexpr修饰函数


constexpr int sub(int i)
{
    return i - 1;
}

constexpr int a = 1;        // 1 是常量表达式
constexpr int b = a + 1;    // a + 1 是常量表达式
constexpr int sz = sub(b);    // sub是一个constexpr函数,这是一条正确的声明语句
int arr[sub(b)] = { 0 };    // 编译器允许这样的定义,并将sub(b)优化成1。C++11之前这是非法的

2.constexpr修饰类

constexpr可以修饰类的构造函数
即:保证传递给该构造函数的所有参数都是constexpr,那么产生的对象的所有成员都是constexpr。
该对象也是constexpr对象了,可用于只使用constexpr的场合。
**注意**constexpr构造函数的函数体必须为空,所有成员变量的初始化都放到初始化列表中。

class Test
{
 public:
    constexpr Test(int arg1, int arg2) : v1(arg1), v2(arg2) {}
 private:
    int v1;
    int v2;
};

constexpr Test A(1, 2)
enum e = { x = A.v1, y = A.v2 };

3.constexpr递归函数


constexpr int fibonacci(const int n)
{
    return n == 1 || n == 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}

从 C++14 开始,constexptr 函数可以在内部使用局部变量、循环和分支等简单语句

例如下面的代码在 C++11 的标准下是不能够通过编译的

constexpr int fibonacci(const int n)
{
    if (n == 1) return 1;
    if (n == 2) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

4.使用constexpr的好处

  1. 很强的约束,保证程序的正确定义不被破坏;
  2. 编译器对constexper代码进行了优化,例如:将用到的constexpr表达式直接替换成结果;
  3. 相比宏来说没有额外的开销。

还可以参考:https://blog.csdn.net/weixin_40087851/article/details/82754189

                      https://www.jianshu.com/p/34a2a79ea947

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值