【C++初阶】extern C,引用,内联函数,auto和指针空值

x33g5p2x  于2021-10-14 转载在 C/C++  
字(4.9k)|赞(0)|评价(0)|浏览(410)

1.extern “C”

我们知道,在C代码当中可以调用C语言部分,但是在C语言编写的代码当中无法直接调用纯C语言编写的内容,如何解决这一问题呢?
在C模块前加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C的,这样C语言便可以调用这部分C++编写的模块。

比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()tcfree()
两个接口来使用,但如果是C项目就没办法直接使用,这时候就可以使用extern “C”来解决。

实例

//引入extern "C" 是告诉编译器其修饰下的函数的命名修饰规则按照C语言下的命名规则进行
//即此时的函数add在转化到符号表中后为_Add 而非 _Z3Addii
 extern "C" int add(int a, int b)
{
	return a + b;
}

int main()
{
	int ret=add(1, 2);
	return 0;
}

2.引用

2.1 引用的概念

引用不同于指针,引用并不会定义新的变量,而是给原来的变量起一个“别名”,就好像华为公司,又被称为“菊厂”、“沸腾厂”等,而这些称呼指的对象都是华为公司,本质是一样的。因此编译器并不会为引用变量单独开辟一块内存空间,该变量与它引用的变量共用同一块内存空间。
引用的符号为&,其使用方法是 类型 + & + 引用变量名称(对象名) = 引用实体; (引用实体的类型必须和&前的类型保持一致)

示例代码:

int main()
{
	// 一定要注意这里跟C取地址用了一个符号 &
	// 但是他们之前没有关联,各个各用处
	int a = 10;
	int& b = a;
	int& c = a;
	int& d = b;

	c = 20;
	d = 30;

	int& e;

	return 0;
}

观察到初始值均为10

当执行完 c=20之后,均为20

当执行完 d=30之后,均为30

2.2 引用的特性

1.引用在定义同时必须要初始化。比如在取别名的时候就需要有这个被取别名的对象。
2.一个引用实体可以有多个引用。
3.一旦引用变量成为了一个实体的引用,就不能再成为其他实体的引用。

示例代码:

int main()
{
    int a = 10;  
    int d = 100;
    
    int& b = a; 
    //一旦写了引用,就必须有完整的实体,不能写成  int& b;  这是不允许的,即第一条特性
    
    int& c = a; 
    //a变量被引用了两次,也就是第二条特性意思
    
    c = d;
    //前面c已经成了a的别名,那么c就永远只能是a的别名,只不过这里C的值变成了100(同样ab也变成100).   第三     条特性意思

    return 0;
}

来一道测试题看看,分别画出分割线之前和之后各个变量的图示

int x = 0,y = 1;
int* p1 = &x;
int* p2 = &y;
int*& p3 = p1;
/————————————————分割线——————————————/
*p3 = 10;
p3 = p2;

分割线之前的:

分割线之后的:P3、P1均指向了y

2.3 常引用

常数无法直接引用,引用前需要加上const构成常引用,如下:

const int a = 10;
int& ra = a;    //该句代码编译出错,因为a为常量
const int& ra = a;  //成功引用

int& b = 100; //引用失败,因为100是常数,无法int引用
const int& rb = 100;  //成功引用

总结:可以缩小读写权限,但不能放大读写权限.

2.4 引用的使用场景

(1)作参数

根据以上特性,引用在作参数过程中,引用能发挥哪些作用呢?

1.可以减少传参拷贝(引用作用)
2.可以保护形参不被修改,既可以接收变量,又可以接收常量(常量引用作用).

代码1:减少传参拷贝

struct node   //某个结构体,假设他很大
{
    int val;
    struct node* next;
};

//某函数定义如下: 如果其参数设置为引用,将不需要通过函数传递方式中的值传递(拷贝),造成空间消耗巨大.
void modify(struct node& node0) 
{
    //此处省略相关操作....
}

代码2:保护形参不被修改,既可以接收变量,又可以接收常量

int add(const int& a,const int& b)
{
    return a-b;  //比如加法函数,如果手误,码码错代码,修改了a或b的值,编译器会自动提示.
}

int main()
{
    int a = 10;
    int b = 20;
    
    cout<<"变量作为实参"<<add(a,b)<<endl;
    
    cout<<"常量作为实参"<<add(10,20)<<endl;   //必须是常量引用,否则将无法接收实参.
    return 0;
}

(2)作返回值

首先来看看下面这段代码是否正确:

int Add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int a = 10;
	int b = 20;
	int& ra = Add(a, b);
	cout << ra << endl;
}

编译器对上述程序会报错!这是啥原因?

函数调用是会建立栈帧的,而栈帧在函数调用结束后会销毁并将这块栈帧还给操作系统,那么函数中创建的变量也会被销毁。

但是为什么函数的返回值还能被接收呢?

这是因为系统会创建一个临时变量,函数返回值会赋给这个临时变量,同时这个临时变量又会赋给要赋给的变量;而这种临时变量具有常性只能赋给了一个类型为const int 的临时变量,而ra作为int类型的引用,显然是无法接收这个具有常性的临时变量

注意:当引用作为函数返回值时,被引用的对象其作用域必须是有效范围,所以返回一个对局部变量的引用是不合法的,应该是返回值为全局变量或则static修饰的变量.

引用作为引用实体的别名,没有独立空间,与实体共用同一块空间,但是在底层实现上,引用和指针的实现方式是一样的(可以通过编译器反汇编观察到)。

2.5 那么引用与指针有哪些区别?

(1)引用需要初始化,而指针没有要求。
(2)引用一旦作为一个引用实体的引用,就不能再作为其他实体的引用,但指针可以修改其所指向的对象的。
(3)引用没有独立空间,而指针有,但是引用的效率也会更高(毕竟少开辟了一大块内存空间)。
(4)对于sizeof,引用变量的大小与类型有关,指针变量的大小与类型无关。
(5)对于自加,引用加一是数值上加一,而指针加一是跳过一个类型的大小。
(6)访问实体的方式不同,指针是通过解引用访问,而引用是编译器自己处理。
(7)引用使用起来相对于指针更安全。

3.内联函数

3.1 概念

一种通过inline修饰的函数,C++编译器进行编译时可以直接在函数调用的地方进行展开,减少了多余的函数栈帧开销,提高了程序运行效率

3.2 三个特性

一、 inline 是一种以空间换时间的做法,省去调用函数的开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
二、inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline 的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
三、inline 不建议声明和定义分离,分离会导致链接错误。因为inline 被展开,就没有函数地址了,链接就会找不到。

4.auto关键字

4.1 概念

一个新的类型指示符,auto声明的变量必须由编译器在编译时期推导而得.

int main()
{
	int a = 10;
	auto b = a;   //编译器会自行推导数据类型
	auto c = 'c'; //编译器会自行推导数据类型
	auto d = 3.0; //编译器会自行推导数据类型
	cout << typeid(b).name() << endl;//显示变量类型
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}

4.2 auto的使用细则

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

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

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

4.2.2 在同一行定义多个变量

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

int main()
{
	auto a = 10, b = 20;
	auto c = 3, d = 4.0;//编译错误,c和d的初始表达式类型不同
	return 0;
}

4.3 auto不能推导的场景

4.3.1 auto不能作为函数的参数,不能直接用来声明数组.

void TestAuto(auto c)//错误
{
	int a[] = {1,2,3};
	auto b[] = {4,5,6};
}

4.3.2 auto声明的变量不能作为函数的形参类型

这是因为在编译阶段编译器无法推导形参的类型。

int Count(auto n)//错误
{
	return 5;
}

int main()
{
	int n = 0;
	Count(n);
	rrturn 0;
}

5.指针空值nullptr

在良好的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

可以看到,在C++中,NULL被定义为字面常量0,其他情况下,NULL被无指针类型(void / )的常量0,无论使用何种定义,在使用NULL作为空指针时,总会不可避免的出现一些问题*。

比如下例程序,大家现在猜猜输出结果会是啥?

void f(int x)
{
	cout<<"f(int)"<<endl;
}
void f(int* x)
{
	cout<<"f(int*)"<<endl;
}

int main()
{
	f(0);
	f(NULL);
	f((int*)NULL);       
	return 0;
}

我们传参NULL时候,本意是想调用第二个函数,但是编译器却认为我们想要调用第一个函数,这就是在C语言中使用NULL的缺陷,因此,C++提出了nullptr代替NULL
在C++98中,编译器默认将NULL看成一个整型常量,在函数重载的作用下,编译器无法通过NULL来调用参数为指针类型的Test函数,这会产生歧义。如果要将其按照指针方式来使用,必须对其进行强转(void /* )0。

需要注意:

1.nullptr在C11中是作为新关键字引入的,因此在使用其表示空指针时,无需包含头文件。
2.在C
11中,sizeof(nullptr)与sizeof((void/* )0)大小相同。
3.为了提高代码的健壮性,后续代码中表示指针空值时最好使用nullptr。

6.习题(选择题,别慌!)

练习题1

解析:

练习题2:

解析:

练习题3:

解析:

练习题4:

解析:

练习题5:

解析:

练习题6:

解析:

练习题7:

解析:

练习题8:

解析:

练习题9:

解析:

C++的extern “C”、引用、内联函数、auto内容到此介绍结束了,感谢您的阅读!!!如果内容对你有帮助的话,记得给我三连(点赞、收藏、关注)——做个手有余香的人。

相关文章