初识C++(C++入门)

命名空间

写C++的程序时,通常在程序的开头会写上这两行代码

#include <iostream>
using namesapce std;

而为什么要写using namespace std;这行代码?namespace是c++的命名空间,std是命名空间的名字,是库中的一个标准命名空间,里面包含了iostream头文件中的所有函数定义。

c程序中,定义函数基本都是定义全局函数,这样做会存在命名重复,导致命名冲突和名字污染。c++为解决这个问题引入了命名空间的概念,命名空间能改变变量和函数的作用域,这样即使名称相同,作用域不一样,调用的对象便是不一样的。

而命名空间能自己定义在这里插入图片描述
这样就创建一个N1的命名空间。

1.命名空间可以定义变量,能对变量赋值,也能定义函数
2.命名空间可以嵌套
3.两个命名空间名称相同时,最后会合并成一个命名空间

如何使用命名空间?

一种是:可以像经常写的using namesapce std;一样,using namespace + 命名空间的名称,这样就能使用这个空间里的所有变量与函数,使用方便了,但同时也会有命名冲突的问题。

不用using namespace + 命名空间的名称的话:可以在变量前加上::作用域限定符,例如要使用N1命名空间中的b变量,就写N1::b,使用不方便了,但这样写使得命名冲突的概率减小
在这里插入图片描述

using namespace N1;// 将N1命名空间的所有变量和函数全放出来
using N1::b; // 只放N1命名空间中的b,减小命名冲突的概率

流插入与流提取

c++使用cout流插入运算符,cin流提取运算符,输入与输出变量时不用加上像c语言一样的格式限定符%d,%s…但在使用时要加上iostream这个头文件,因为cin和cout是定义在这个头文件中的
在这里插入图片描述

缺省参数

调用函数时,若不给定函数参数,函数会使用默认的参数。默认参数是在定义时就指定的,下面的代码是定义一个有缺省参数的函数

void Func(int a = 10)
{
	cout << a << endl;
}

调用该函数时,不给变量a时,函数默认打印10,若给了a打印的是a的值。

应用场景

void StackInit(Stack* st, int n = 4)
{
	st->data = (int*)malloc(sizeof(int) * n);
	st->size = 0;
	st->capacity = n;
}
int main()
{
	Stack st;
	StackInit(&st, 10);
	return 0;
}

之前写的栈有一个栈初始化的函数,可以给该函数两个参数,一个是栈的地址,一个是要初始化的空间大小n,若初始化时不给n的值,函数默认开辟4个int大小空间给栈空间。


注意点:缺省参数只能从右向左缺省

void Func(int a, int b, int c = 10, int d = 9)// 从右向左缺省
{
	...
}

如上面这个函数只能缺省后两个参数,不能只缺省前面的参数后面的参数不缺省。

如Func(1, 2), a给1,b给2,c和d给的值就是默认值。

void Func(int a = 10, int b, int c, int d)// error,不能只缺省前面的参数,后面参数不缺省
{
	...
}

而一个函数是从左向右缺省的,当你不想给a这个参数的时候,该如何调用?

是像Func( , 1, 2, 3);这样?但c++的语法规定不能这样调用函数。所以一个函数的参数缺省只能从右向左,这样程序才知道哪些参数是缺省的。


注意点:当写多文件项目时,**缺省参数在声明给!**也就是在头文件中给缺省参数的声明,在实现函数时可以不写成缺省参数。

// test.h
#include <stdio.h>
void Func(int a, int b);// 缺省参数的声明
// test.c
#include "test.h"
void Func(int a, int b = 10)// 在定义时定义成缺省参数
{
	printf("a = %d, b = %d“, a, b);
}
// main.c
#include "test.h"
int main()
{
	int a = 1;
	Func(a);// 调用缺省参数
	return 0;
}

若像上面的代码一样,声明函数时声明成普通函数,定义函数时却不声明其缺省了参数。在程序编译后, test.h头文件展开,在main.c这个文件中,编译器看到test.h的声明,认为Func是一个普通函数有两个参数,但main函数调用Func时只给了一个参数,参数太少,显然会报错。
在这里插入图片描述
而在声明将函数的缺省参数声明出来(函数实现时不用再声明缺省参数了,程序会报错:重定义默认参数),编译器就知道这是一个缺省参数的函数,在main中只传一个参数是符合语法规定的,所以程序能正常通过。

函数重载

c++允许在一个作用域中声明几个作用类似的同名函数,构成函数重载的条件除了函数名相同还有

  1. 参数类型不同
  2. 参数顺序不同
  3. 参数个数不同

注意:返回值不同是不构成重载函数的。

举例

void Func(int a, int b);
void Func(int b, int c);// 这一组函数不构成重载,参数类型相同,且顺序一样(参数名称不同是不构成重载的)

void Func(int a, int b);
int Func(int a, int b);// 返回值不同也不构成重载

void Func(int a, int b);
void Func(double a, doube b);// 参数不同,构成重载

void Func(int a, double b);
int Func(double a, int b);// 参数类型不同,顺序不同,重载

void Func(int a, int b);
void Func(int a, int b, int c);// 个数不同,构成重载

C++是怎样做到函数重载的?

先要知道程序编译链接的过程

  1. 预处理:宏替换,去注释,条件编译,头文件展开
  2. 编译:检查语法,将代码翻译成汇编代码
  3. 汇编:将汇编代码转化成二进制代码
  4. 链接:找代码中函数的地址,与链接对应上,合并到一起

在第4步的链接中,程序是怎么找函数调用的地址的?

假设现在有三个文件

  • 分别对应着代码逻辑的执行 – test.cpp
  • 代码中主要函数的实现 – fun.cpp
  • 代码中用到的一些函数需要引用的头文件还有主要函数的声明 – fun.h

在fun.cpp和test.cpp中引用了fun.h这个头文件,在第一步编译时将fun.h这个头文件展开,由于该文件中有fun.cpp实现函数的声明,main.cpp在编译时能正常通过(所以有时程序报的错是编译错误,说明函数的声明不完整,程序不知道有这个函数,自然报错)。声明相当于承诺,告诉程序有这个函数,程序记住这个函数的名字。

在第二步编译过后,cpp文件生成了o文件,同时生成了符号表和函数调用指令。

符号表中有函数名与其函数地址的映射,知道函数的名字就知道函数的地址,就能调用该函数。函数调用指令就是字面意思,告诉程序要去调用这个指令。

例如在test.cpp中你调用了Func这个函数,并且fun.h中有这个函数的声明,所以在最后的链接中,程序接收到要去调用Func这个函数的指令,但程序只知道函数的名字是Func,所以程序去符号表中去找有没有这个函数名,找到了函数名就拿到了该函数的地址,进而去调用这个函数。

但是你的fun.cpp中没有Func这个函数的定义,生成的符号表便没有Func和其地址的映射,所以程序会报一个链接错误。

总结:在链接时得到函数调用指令后,会去符号表中找对应函数名,若没有该函数名,则链接错误。其中函数名的格式是怎样的?

在C中,符号表里的函数名就是函数名,Func函数在符号表里的函数名就是Func,但在C++中,函数名的命名规则是_Z+函数名长度+函数名+参数类型首字母,Func(int a, int b)在C++的函数名是_Z4Funcii(linux中)。

所以C++中,相同函数名但参数类型不同,符号表中的函数名就不同,顺序不同,个数不同,函数名也不同,但返回类型没有在符号表中体现,所以返回类型不同不构成重载,C中的符号表的函数名就是函数原来的名字,所以相同函数名会导致函数的重定义,不能构成重载。这就是C++能使用C不能使用重载函数原理。

引用

假设定义了一个int类型的数,C++支持对该对象取别名,这个别名叫做该变量的引用。引用符号与C的取地址符相同,定义别名时,在别名类型后面加上一个&(引用类型与引用实体的类型必须相同),类型& 引用变量名 = 引用实体。具体可以看下图。引用与C的指针不同,定义一个指针时开辟了一块内存空间,取别名时并没有开辟空间,引用变量与引用实体共用一块内存空间:取出别名的地址时,别名地址与被取别名的变量的地址相同。所以引用可以理解为取小名,绰号,可以用这个小名去调用它。
在这里插入图片描述
调用引用变量,修改引用变量的值,引用实体的值也被修改。
在这里插入图片描述

引用特性

1.一个变量可以有多个引用

在这里插入图片描述
如果b是对a的引用,c是对b的引用,则c和b都是对a的引用。

2.引用变量在定义时必须初始化

C在定义指针时,可以像定义一个变量一样,不对其进行初始化是被允许的,但里面存的数据是一些乱码。而C++的引用却不能不初始化,因为引用不是一个变量,引用在初始化时必须要有一个确定的引用实体,与其共享内存空间,不对其进行初始化,引用变量该与谁共享内存空间?
在这里插入图片描述

3.引用变量一旦引用了一个实体,便不能再引用其他实体

和C的指针不同,指针作为一个变量,存的是指向对象的地址,改变指针存储的地址,就改变指针指向的对象。但是看下面的代码是什么意思
在这里插入图片描述
首先b是a的引用,但我想把b变成c的引用,我是写b = c 还是写&b = c,很显然两种都不行,b = c,是将c的值赋值给b变量,而b变量又是a实体的引用,这行代码其实是将c的值赋值给a。&b = c,b的地址是一个常量,将c赋值给一个常量?这样的代码明显是错的。由于C++没有其他的修改引用变量的方式,所以一个引用变量一旦引用了一个实体,就不能对其修改,因为没有修改的方法。

4.引用权限只能缩小不能放大

在这里插入图片描述
const修饰的常变量只能读,一般的变量是支持读与写的,当一个引用变量的类型支持读与写,去引用const修饰的实体,就能通过支持读写的引用去修改不支持写的实体,出于安全考虑,这样的权限放大时不被允许的。在这里插入图片描述
一个支持读写的实体被一个const修饰的只支持读的引用变量引用时,权限缩小,不能通过引用变量修改引用实体,这样的操作时安全的,被允许。

引用使用场景

作为函数参数

将引用作为函数的形参在这里插入图片描述
在写交换函数时,将引用作为函数形参,就不用对指针解引用,减少了代码量,也使代码看起来更整洁。

形参num1,num2与实参a,b共用一块内存空间,修改形参就能对实参进行修改。

所以引用作为形参可以是一个输出型参数(例如调用一个函数,返回一个数组,不知道数组的长度就能在函数的形参中写一个引用,该引用作为输出型参数,在函数外就能拿到数组的长度)。

还有就是引用和指针一样,作为函数参数能减少参数的拷贝,提高函数调用效率,减少内存的消耗。

作为函数返回值

由于返回的是一个变量的引用,当这个变量在函数调用完被销毁后,再返回被销毁变量的引用会导致非法访问,这是一个经典的野引用问题在这里插入图片描述
x在函数中被定义,作用域只在函数中,出了函数被销毁,此时访问函数返回的引用,就会导致非法访问。

传值与传引用效率对比

和传指针一样,地址是一串固定的值,而变量有时候很大,传值的时候函数在拷贝的过程中将会浪费大量时间,所以传地址是最好的。而引用也不会拷贝变量,是与引用实体共用内存空间的,所以效率也是比传值高的

#include <time.h>
struct Test
{
	int data[100000];
};

void test1(Test a){}
void test2(Test& a) {}

int main()
{
	Test a;
	int begin1 = clock();
	for (int i = 0; i < 10000; i++)
		test1(a);
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0; i < 10000; i++)
		test2(a);
	int end2 = clock();

	printf("传值调用:%d\n传址调用:%d", end1 - begin1, end2 - begin2);
}

在这里插入图片描述

重点+细节点:引用产生的临时变量

在介绍引用的最后,还有一个重要的知识点

double d = 1.0;// 双精度浮点数,大小为8字节
int i = d;// 整形大小为4字节

d作为一个浮点数能直接赋值给整形变量i吗?(连类型大小都不同要怎么复值?),答案是不行,由于浮点数与整形的存储机制不同,直接进行二进制数的拷贝呢,会丢失原来的数据(如果你对其中的原理感兴趣,可以看数据在内存中是怎样存储的?这篇文章),所以编译器做了这样的一个处理:d在赋值给i之前,先将1.0的整数部分1赋值给了一个临时变量,这个临时变量类型是int,再由临时变量赋值给i。

临时变量的最重要的一个性质就是:临时变量具有常属性,就像用const修饰的变量一样,不能改变临时变量的值。

所以隐式类型转换的过程中会产生一个临时变量

double d = 1.0;
int& i = d;// 这样的引用可以吗?

答案是不行
在这里插入图片描述
错误信息中的非常量限定是什么意思?这就和临时变量有关系了,i作为d的引用,但i的类型是int,d是double,不能直接引用,所以编译器生成了一个临时变量,类型为int,将d的整数部分给了该临时变量。所以准确的说i并不是d的引用,而是该临时变量的引用。而临时变量具有常属性,只读,不能修改,用int去引用一个具有常属性的变量显然是不行的(因为int支持读写,常属性只能读),所以程序会报错"int& 非常量限定"。正确的写法是

double d = 1.0;
const int& i = d;

还能通过i和d的地址不同,证明i不是d的引用
在这里插入图片描述
i是临时变量的引用,临时变量具有常属性,引用变量必须加上const!

内联函数

内联函数是以inline修饰的函数,程序在编译后内联函数会被展开,减少函数的栈帧创建时间,提高程序运行的效率。

例如

inline void Func()
{
	printf("Hello, world!");
}
int main()
{
	Func();// 在main函数中调用内联函数
	return 0;
}

程序在编译后是这样的,函数部分会被展开

int main()
{
	printf("Hello, world!");
	return 0;
}

编译后被展开,这一点和C中的宏替换一样,比如我写一个Add的宏

#define Add(X, Y) ((X) + (Y))
int main()
{
	Add(3, 4);
	return 0;
}
int main()
{
	((3) + (4));// 被展开后是这样的
	return 0;
}

但是由于宏替换没有对参数进行类型检查,以及要对替换内容繁琐的加上括号等等一些细节点,导致宏替换很容易出错,为弥补这个坑,C++使用了内联函数,写一个函数出错的概率总是小于写一个宏的。只需要在函数前面加上一个inline就能达到宏替换的效果,所以内联函数比起宏替换更好用。

在vs中,Debug和Release都默认将内联函数优化,即将内联函数看成普通函数来调用而不展开,要想在调试时看到内联函数的展开要设置一下
在这里插入图片描述
在汇编中看到代码转化后的结果出现了call指令,说明Func被当成了一个普通函数去调用。在这里插入图片描述
在这里插入图片描述
选择C++,常规,将调试信息格式改成程序数据库
在这里插入图片描述
再选择优化,将内联函数扩展改成只适用于inline。点击应用
在这里插入图片描述
此时调试程序,右键选择转到反汇编就能看到汇编代码
在这里插入图片描述这时的Func函数中就没有call Func这个指令(上面的call _printf是库函数的调用)

内联特性

1.内联是一种以空间换时间的做法,所以当内联函数的代码量大时(如行数超过10行,递归),则不建议使用内联
2.内联只是一种建议,当函数的代码量大时,编译器会自动将内联优化成普通函数
3.内联函数的定义和声明不能分开,否则会出现链接错误

Func函数的行数较多时,编译器是否会展开内联函数?
在这里插入图片描述
在这里插入图片描述
答案是你的内联函数只是一个建议,编译器会根据具体的情况做出优化

还有就是当写一个大的项目时,内联函数的定义要写在h文件中,千万不能和普通函数一样,h中写声明,其他文件中实现,因为编译器看到inline就不会将这个函数的地址放到符号表中,程序能编译通过,但是在链接时找不到函数的地址。所以内联函数要么实现在h文件中,要么实现在源文件中,不能声明与定义分离。

auto关键字

在C中auto关键字用来修饰自动存储器的局部变量。可以说这个关键字没有什么存在感,即使不写这个关键字,系统默认对局部变量写的就是这个关键字。而在C++11的标准中,auto关键字被赋予了全新的意义:其不再是一个存储类型关键字,而是一个类型指示符(像int那样),指示编译器在编译时去推导其修饰的变量类型是什么。

int Func()
{
	return 10;
}

int main()
{
	int a = 10;
	auto b = a;// 此时的auto相当于int
	auto c = 'a';// auto相当于char
	auto d = Func();// 函数的返回值,auto相当于int
	auto e;// 不能不对auto初始化,error
}

在这里插入图片描述

auto的细节点

auto与指针和引用一起使用

auto* 和auto没有区别,但auto在声明引用时必须写成auto&。

int main()
{
	int a = 10;
	auto b = &a;
	auto* c = &a;// auto声明指针时,加与不加*都没区别
	auto& d = a;// auto在声明引用时必须加上&

	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

在这里插入图片描述

在同一行声明多个变量

int main()
{
	auto a = 3, b = 4;
	auto c = 1, d = 2.0;// error,两个变量必须是同一类型
	return 0;
}

可以在一行声明多个变量,在这种情况下用auto声明的多个变量类型必须相同。因为编译器只对第一个变量类型进行推导,再用推导出的变量去定义剩下的变量。

auto不能使用的场景

auto不能作为函数形参类型的声明。

void Func(auto num);// error,这样写是想对这样的函数传任意类型的参数吗?

这里是因为auto在编译时就进行推导,但是函数没有调用怎么去推导auto,不推导auto的话函数要创建多大的栈帧?所以auto作为函数形参时,编译器无法知道函数要创建栈帧的大小,auto不能使用。


auto不能做函数返回值
假设有这样一个函数,其参数是auto返回值也是auto,那使用者怎么知道这个函数要传什么参数,要用什么类型的对象接收它的返回值,所以这样的一个函数写的太过随意,以至于让人无法使用它,所以在写函数时千万不要使用auto。


auto不能用来声明数组
这个是语法规定,了解即可。

那auto要怎么使用?

auto的使用:范围for

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	// 平常的for循环写法
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << ' ';
	}

	cout << endl;
	// 应用auto
	for (auto& e : arr)
	{
		cout << e << ' ';
	}
	return 0;
}

在这里插入图片描述
第一个for循环是C中的写法,在C++中对于一个有限集合(数组)来说,可以使用范围for来简化C中的写法。范围for被:分成两个部分,:前的是用来迭代的变量,:后的是要迭代的数组。

e是element(元素)的首字母,auto e相当于定义了一个变量e,arr是要迭代的范围。至于e为什么要声明成引用变量,当要用e来修改数组里的数据时,就要使用引用。例如要就数组中的每个数++

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << ' ';
	}

	cout << endl;

	for (auto& e : arr)
	{
		cout << e << ' ';
	}

	cout << endl;

	for (auto& e : arr)
	{
		e++;
	}
	
	for (auto& e : arr)
	{
		cout << e << ' ';
	}
	return 0;
}

在这里插入图片描述


当一个对象的类型名太长时,定义该对象时也可以用auto以提高代码效率

nullptr关键字

为何在C++中使用nullptr而非NULL

通常我们定义一个空指针时,是用NULL来对其赋值,但NULL只是一个宏定义,它可能是0,也可能是无类型指针(void*)0
在这里插入图片描述
上面的宏定义解读:如果是C++的程序,NULL是0,C程序NULL则是无类型指针。

但我在C中做了个测试,将NULL作为函数参数传给func1和func2,两个函数的形参一个是int一个是int*,但程序却没有报错,成功的运行了。所以在C中NULL即能看成字面常量0,也能看成无类型指针(void*)0。
在这里插入图片描述
但C++引入了重载的概念,此时func函数有两个重载,一个形参是int,一个则是int*,这时便会出现歧义,NULL在C中是即可以当成int也能当成int*,那在C++中是怎样的?

void func(int p)
{
	printf("int\n");
}

void func(int* p)
{
	printf("int*\n");
}

int main()
{
	func(0);
	func(NULL);
	func(nullptr);

	return 0;
}

在这里插入图片描述
可以看到C++中NULL是字面常量0,但NULL不应该是一个指针吗?所以C++引入了关键词nullptr,nullptr始终是一个空指针,所以为了提高代码的规范性,在C++中推荐使用nullptr不要使用NULL。

在64位平台下,一个指针的大小是8字节,在C++中nullptr与(void*)0,的字节大小一样,因为这两都是指针,但NULL却是一个整形,大小是4。
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值