【C++入门基础篇】---- 万字解析


此篇为C++入门首篇,学习的主要是:命名空间 || C++输入和输出 || 缺省参数 || 函数重载 || 引用 || extern "C" || 内联 || auto关键字(C++11) || 基于范围的for循环(c++11) || 指针空值nullptr(C++)
等内容,此次让小新带领大家一起走向C++第一步


壹 ----  为什么要用命名空间 ?

 C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染namespace关键字的出现就是针对这种问题的。 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名 空间的成员。

 0x01 :命名冲突域

   ①C++是兼容C的,所以cpp中一样可以使用c的语法

#include<stdio.h>
int rand = 0;
int main()
{
	printf("hello yongheng\n");
	printf("%d\n,rand");
}

②在stdlib库中有一个产生随机数的函数rand()

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int main()
{
	printf("hello yongheng\n");
	printf("%d\n",rand);
}

③我们知道 #include包含头文件,头文件会被展开来,此时stdlib库展开,其中包含了一个rand的函数,如果此时再定义一个rand全局变量会是什么结果呢?

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main()
{
	printf("hello yongheng\n");
	printf("%d\n",rand);
}

结果马上揭晓:
没错就是下面这个结果

 
④那么这个问题该如何解决呢?

        此时的良方就是运用命名空间,因为它解决的就是命名冲突问题,namespace中的rand还是全局变量,此时namespace的作用就是为了防止stdlib库中的函数rand与变量rand命名冲突

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace yongheng
{
	int rand = 0;
}
int main()
{
	printf("hello yongheng\n");
	printf("%d\n",rand);
}

⑤可以通过 作用域限定符进行验证,是否可以显示namespace中变量rand,需要了解的是,机器首先会在局部中去找,然后再到全局中去找,如果不使用作用域限定符的话是不会去namespace中去寻找rand变量的,因为namespace创建的是一个域,相当于将rand变量包裹在这个域中了

0x02: 命名空间的内容
       ①变量 ②函数 ③结构体 等 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace yongheng
{
	int rand = 0;//变量
	int Add(int left,int right)//函数
	{
		return left + right;
	}
	struct Node //结构体
	{
		struct Node* next;
		int val;
	};

}
int main()
{
	printf("%d",yongheng::rand);
	yongheng::Add(1,2);
	struct yongheng::Node node;
}

0x03: 命名空间可以嵌套命名空间

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
namespace N1
{
	namespace N2
	{
		int Add(int left,int right)
		{
			return left + right;
		}
	
	}

}
int main()
{
	printf("%d",N1::N2::Add(1,2)); // 3
}

0x04: 同一个工程允许多个相同名称的命名空间,编译器最后会将其合成同一个命名空间

示例如下:


注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

0x05  命名空间的使用

   ① 加命名空间名称及作用域限定符

  ② 使用using namespace命名空间名称引入

全部展开,用起来方便了,隔离就失效了,所以应该谨慎使用

 ③ 使用using 将命名空间中某个成员引入

//单独展开某一项,用于展开命名空间中常用的
using N1::N2::Node;
int main()
{
	struct Node node;
}

0x06:  三种命名空间的使用示例

   iostream库包含了输入输出流,std是一个标准库命名空间,里面包含了cout(流插入)和cin(流提取)等

 ①将std命名空间展开(适合练习的时候使用)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;//
int main()
{
	cout << "hello world" << endl;
	return 0;
}

②std命名空间中包含cout与cin和endl等,所以可以指定展开

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
int main()
{
	std::cout << "hello world" << std::endl;
	return 0;
}

③将常用的展开

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using std::cout;
using std::endl;
int main()
{
	cout << "hello world" << endl;
	return 0;
}

贰 ----  C++输入和输出

0x07  cout和cin的运用

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
	int i = 0;
	int j = 3.14;
	cout << i << ' ' << j << endl;
	cin >> i >> j;
	cout << i << ' ' <<  j  << endl;
}

   解析如下:

说明:
1. 使用 cout 标准输出 ( 控制台 ) cin 标准输入 ( 键盘 ) 时,必须 包含 < iostream > 头文件 以及 std 标准命名空间。
注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文 件不带 .h ;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此 推荐 使用 <iostream>+std 的方式。
2. 使用 C++ 输入输出更方便,不需增加数据格式控制,比如:整形 --%d ,字符 --%c

0x08: C语言和C++语言的输出比较
示例如下:

上图可以看出,如果只输出数值的话,还是C++更方便一些,C语言保留了6位小数。而如果结构体student中的成员越多的话,还是C语言的输出语句更方便一些,所以C语言和C++哪个更方便就可以选用哪个。

叁 ---- 缺省参数

缺省参数是 声明或定义函数时 为函数的 参数指定一个默认值 。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。

0x09 全缺省参数

示例如下:

 

 全缺省参数: 即上述a,b,c三个都有缺省参数

0x10 半缺省参数

示例如下:


需要注意的是:
1. 半缺省参数必须 从右往左依次 来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现
//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}
// 注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值。
3. 缺省值必须是常量或者全局变量
4. C 语言不支持(编译器不支持)

肆 ----  函数重载

函数重载 : 是函数的一种特殊情况, C++允许在同一作用域中声明几个功能类似的同名函数 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同 ,常用来处理实现功能类似数据类型不同的问题

0x11  参数类型不同

0x12  参数个数不同

 

0x13  参数顺序不同

0x14  是否构成重载的几种情况

①返回值不同,不能构成重载

②缺省值不同,不能构成重载

 ③构成重载,但是一个存在默认参数,一个不存在默认参数,如下示例,会产生二义性

0x15 为什么C语言不支持函数重载,而C++支持函数重载?

   解析如下:

         首先我们先建立一个头文件 fac.h,再建立俩个文件fac.c和test.c ,然后我们我们回顾一下编译器编译的整个过程:
1. 预处理: 进行的是头文件展开,宏替换,条件编译,去掉注释等等 生成fac.i 和  test.i
2. 编译: 进行检查语法,生成汇编代码  生成fac.s 和  test.s
3. 汇编: 汇编代码转换成二进制机器码  生成fac.o 和 test.o
4. 链接: 生成a.out

① c语言不支持函数重载:因为在编译的时候,俩个重载函数,函数名相同,在fac.o符号表中存在歧义和冲突,其次链接的时候也存在歧义和冲突,因为它们都是直接使用函数名去标识和查找的,而重载函数,它们的函数名又是相同的,所以不支持重载
②c++支持重载函数:因为C++的目标文件符号表中不是直接用函数名来表示和查找的,而是使用函数名修饰规则去查找(在不同编译器下不同),有了这个函数名修饰规则,只要参数不同,fac.o符号表里面重载的函数就不存在二义性和冲突了,所以在链接的时候,test.o的main函数里面去调用这俩个重载的函数,查找他们的地址时,也是非常明确,且没有歧义的

需要注意的是:
①如果当前文件有函数定义,那么在编译的时候就把地址填上了
②如果在当前只有函数的声明,那么定义肯定是在其他的.cpp中,编译时是没有地址的,只能在链接的时候去其他的.o符号表中根据函数名修饰规则去找,这是链接部分所做的
③函数名修饰规则: _z + 函数名长度 + 函数名 + 参数首字母 如 函数名 f() :   _z1fv

伍 ---- 引用

引用 不是新定义一个变量,而 给已存在变量取了一个别名 编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:
李逵 ,在家称为 " 铁牛 " ,江湖上人称 " 黑旋风

  实例如下:

 类型& 引用变量名(对象名) = 引用实体

引用在语法层上来讲是没有开辟新的空间的,就是对原来的空间取了一个新名称叫做b
注意:引用类型必须和引用实体同种类型

0x16 引用特性

   ① 引用在定义时必须初始化及一个变量可以有多个引用

  ②同一个域中不能有相同的名字

 d是a的小名,那就不能有一个人的大名叫d,即同一个域中不能有相同的名字

 ③引用一旦引用一个实体,再不能引用其他实体

④引用与指针的对比

指针可以看作是一个渣男,第一次喜欢A,第二次又喜欢B. 而引用一旦喜欢上A,他就不可能喜欢B了

0x17 传指针、传引用、传值

//传指针
void Swap(int* p1,int* p2)
{
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
//传引用,引用做参数,把a,b起个别名
void Swap(int& r1,int& r2)
{
	int temp = r1;
	r1 = r2;
	r2 = temp;
}
//传值
void Swap(int r1, int r2)
{
	int temp = r1;
	r1 = r2;
	r2 = temp;
}
//他们三个构成重载(类型不同)
//但是Swap(a,b);调用是存在歧义,他们不知道调用,传值还是传引用

int main()
{
	int a = 10, b = 20;
	Swap(&a,&b);
	/*Swap(a,b);*/
}

 0x18 传引用作参数的应用

 0x19 传引用作返回值

注意: 如果函数返回时,出了函数作用域,如果返回对象还未还给系统(全局变量、静态区),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回,不能使用传引用返回.

0x20 常引用

//常引用
int main()
{
	//权限放大: a是只读的,而b是a的引用,竟然是可读可写的,那是不可以的
	const int a = 10;
	int& b = a;

	//权限不变: c和d都是只读的 ,可以
	const int c = 10;
	const int& d = c;

	//权限缩小:f是只读的,可以
	int e = 10;
	const int& f = e;
}

0x21  const在引用中的应用

 上图解析:

第一个图解析:d是double类型,产生临时变量,通过隐式类型转换,变为Int类型,第二个是d相当于是不可修改+int 类型,所以要加const

第二个图解析:x1 + x2 这个结果会产生一个临时变量,临时变量是具有常属性的,不可修改,所以 所以这个临时变量可以看作是一个int类型的数据+不可修改这个特性,所以ret 前面要加const

0x22 引用和指针的不同点

1. 引用 在定义时 必须初始化 ,指针没有要求
2. 引用 在初始化时引用一个实体后,就 不能再引用其他实体 ,而指针可以在任何时候指向任何一个同类型实体
3. 没有 NULL 引用 ,但有 NULL 指针
4. sizeof 中含义不同 引用 结果为 引用类型的大小 ,但 指针 始终是 地址空间所占字节个数 (32 位平台下占4 个字节 )
5. 引用自加即引用的实体增加 1 ,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同, 指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
9.引用概念上定义一个变量的别名(没开辟空间),指针存储一个变量的地址
10.底层汇编实现时:引用是用指针实现的

六 ---- extern "C"

有时候在 C++ 工程中可能需要 将某些函数按照 C 的风格来编译 在函数前加 extern "C",意思是告诉编译器, 将该函数按照 C 语言规则来编译。

0x23 c++项目调用C静态库

1.包含头文件
2.在工程属性中配置静态库的目录和添加静态库
注意:可以理解为,只有c++认识extern C

 步骤如下:

0x24 C工程调用C++库步骤

解析:只有C++认识extern "C",静态库为C++文件时,告诉机器按C的修饰规则去找,而C项目中,头文件展开,直接就是函数名

总结:
C++程序调用C的库,在C++程序中加extern"C"
C程序调用C++的库,在C++库中加extern"C"

 柒 ---- 内联

因为调用函数,需要建立栈帧,栈帧中要保存一些寄存器,结束后又要恢复,频繁的调用函数,这样的操作是有消耗的。而C和C++都有应对的措施。

0x25   C语言通过宏(一种替换,不需要建立栈帧)

#define  Add(x,y) ((x)+(y))
//最好把((x)+(y))中x和y括号都加上,否则比如(x+y),10*Add(3,4),会替换为10 * 3+4,会导致出错

int main()
{
	cout << Add(1, 2) << endl;
	return 0;
}

0x26  inline 内联函数

C++ 以 inline 修饰 的函数叫做内联函数, 编译时 C++ 编译器会在 调用内联函数的地方展开 ,没有函数压栈的开销, 内联函数提升程序运行的效率。

查看方式:

1. release 模式下,查看编译器生成的汇编代码中是否存在 call Add
2. debug 模式下,需要对编译器进行设置,否则不会展开 ( 因为 debug 模式下,编译器默认不会对代码进行优化 )

0x27  inline特性

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

 八 ---- auto关键字(C++11)

在早期 C/C++ auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量 ,但遗憾的是一直没有人去使用它,大家可思考下为什么?
C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得

实例如下:

 

 0x28  实例

int main()
{
	const int a = 0;
	int b = 0;
	//自动将a的类型推到c,  
	auto f = a;//int
	auto c = &a;//int const*
	auto d = 'A';//char
	auto e = 10.11;//double
	//f,d,e的类型是const的,具有常性,但是auto会忽略这个const

	//typeid打印变量的类型
	cout << typeid(f).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	cout << typeid(e).name() << endl;

	auto e;//err,auto定义变量时必须对其进行初始化
}
[注意]
使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型 。因此 auto 并非是一种 类型 的声明,而是一个类型声明时的 占位符 ,编译器在编译期会将 auto 替换为 变量实际的类型

0x29  auto的使用

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

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

        ②   在同一行定义多个变量

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
 auto a = 1, b = 2; 
 auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

0x30  auto 不能推导的场景

        ① auto不能作为函数的参数

        ② auto  不能直接用来声明数组

玖 ---- 基于范围的for循环(c++11)

0x31 范围for的语法

在C/C++中遍历数组可以有如下的方式

    //语法糖
	int array[] = {1,2,3,4,5};
	//c/c++遍历数组
	for (int i = 0; i < sizeof(array)/sizeof(int);i++)
	{
		cout << array[i] << endl;
	}
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环 for 循环后的括号由冒号 分为两部分:第一部分是范围内用于迭代的变量, 第二部分则表示被迭代的范围
    //C++11 范围for
	//自动依次取数组array中的每个元素赋值给e
	for (auto e:array)
	{
		cout << e << endl;
	}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环

 那如何运用范围for将数组中的每个值+1呢?

    for (auto e : array)
	{
		e++;//此时是array数组将每个值赋值给e,e的改变并不会影响到array数组中的值
	}

这个方法是不行的,因为此时是array数组将每个值赋值给e,e的改变并不会影响到array数组中的值,那怎样才能改变呢?可以用引用的方法,数组中的每个值赋值给e变量,而e变量又是该值的别名,修改的是同一份空间,所以会改变数组的值,而应该注意的是每次的e的赋值的作用域只在一个循环之中。这种方法是可以解决这个问题的,代码如下:

	for (auto& e : array)
	{
		e++;
	}

0x32 范围for 的使用条件

for 循环迭代的范围必须是确定的 :
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供 begin end 的方法, begin end 就是 for 循环迭代的范围。
void TestFor(int array[])
{
	for (auto& e : array)
		cout << e << endl;
}

int main()
{
	int array[] = { 1,2,3,4,5 };
	TestFor(array);
}

 以上的代码是有问题的,for的范围是不确定的,因为范围for的范围必须是数组名,而数组传参,传过去之后会退化为指针,所以其实TestFor的array是一个指针,所以是有问题的,应当注意

 十 ----  指针空值nullptr(C++)

NULL其实是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

此时NULL可能被定义为常量,或者被定义为无类型指针(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(0)对应的都是f(int),而程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。而在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0, 所以在C++11中,引入了新关键字nullptr。

注意:

1. 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr C++11 作为新关键字引入的
2. C++11 中, sizeof(nullptr) sizeof((void*)0) 所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用 nullptr
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值