c++入门

发布时间:2024年01月03日

目录

1.命名空间

1.1命名空间定义

1.2命名空间引用

2.?输入输出

3.缺省参数

?4.函数重载

4.1编译器原理(部分)

5.引用

5..1用途

5.1.1用途1:传参

5.1.2返回值

5.2引用和指针

6.内联函数

7.auto

8.范围for

9.空指针


1.命名空间

c语言对于名字冲突的问题,无法解决,所以在c++中有了命名空间,用来解决命名冲突的问题,事实上很多库里自己就有定义一个函数名,我们如果在源程序的全局作用域里定义了相同的名字(可能是函数,也可能是作为一个变量名),这时候,就会出现问题。

假如我们引用了c的rand函数头文件,但自己又定义了一个rand变量,这时就会冲突

1.1命名空间定义

namespace zsl
{
	int rm = 1;
	int vb(char a)
	{
		//.....
	}
	struct Node
	{
		int a;
	};
}
可以定义变量、函数、类型
namespace zsl
{
	int rm = 1;
	int vb(char a)
	{
		//.....
	}
	struct Node
	{
		int a;
	};
	namespace zsl111
	{
		int c = 2;
	}
}
可以嵌套定义

注意,一个工程文件里,可能有头文件、源文件,在相同工程文件的不同文件里,如果我们定义了相同名称的命名空间,最后编译器会将其合并到同一个命名空间

关于这点,假如我们手搓了一个栈,但是为了防止在实际写项目时出现冲突(比如别人也定义了一个栈,那栈的操作函数就可能引用错误了栈的声明),我们可以在头文件和(定义栈操作函数)源文件里各用相同的命名空间包含它们,这样就会被编译器放在一起。

一个命名空间,就定义了新的作用域,命名空间的所有内容都被限制于命名空间

1.2命名空间引用

#include<stdio.h>
namespace zsl
{
	int rm = 1;
	int vb(char a)
	{
		//.....
	}
	struct Node
	{
		int a;
	};
	namespace zsl111
	{
		int c = 2;
	}
}

int main()
{
	printf("%d", zsl::rm);
	zsl::vb('a');
	struct zsl::Node b;
    printf("%d", zsl::zsl111::c);
	return 0;
}
::是作用域限定符
#include<stdio.h>
namespace zsl
{
	int rm = 1;
	int vb(char a)
	{
		//.....
	}
	struct Node
	{
		int a;
	};
	namespace zsl111
	{
		int c = 2;
	}
}
using zsl::rm;
int main()
{
	printf("%d", rm);
	zsl::vb('a');
	struct zsl::Node b;
	printf("%d", zsl::zsl111::c);
	return 0;
}
using命名空间的某个成员,就不用额外加域限定符,默认引用这块空间的内容
#include<stdio.h>
namespace zsl
{
	int rm = 1;
	int vb(char a)
	{
		//.....
	}
	struct Node
	{
		int a;
	};
	namespace zsl111
	{
		int c = 2;
	}
}
using namespace zsl;
int main()
{
	printf("%d", rm);
	vb('a');
	struct Node b;
	printf("%d", zsl111::c);
	return 0;
}
这样就不用指定zsl了

?如果同时展开了两个命名空间,且有相同的变量,那么调用变量时,会按语句顺序,从先展开的命名空间里找。

2.?输入输出

#include<iostream>
using namespace std;
int main()
{
	cout << "sss"<<endl;
	return 0;
}
std是c++官方库定义的命名空间,要用c++的库,基本都要用到std
里的内容
工程时尽量不要直接展开,因为容易冲突,可以指定展开
平时自己写代码可以直接展开,方便写代码

cout是标准输出对象(控制台),cin是标准输入对象(键盘)
使用时要使用命名空间std,还要包含头文件<iostream>

endl是换行符,也在<iostream>

<<是流插入运算符,>>是流提取运算符

相比c,cin和cout都是自动识别类型的。




要注意的是,我们引用c++的头文件,都不用加.h后缀,是因为,c++是在c的基础上诞生的,为了跟原来的c语言库文件区分,除了多了命名空间外,引用c++头文件外不用加.h后缀

3.缺省参数

#include<iostream>
using namespace std;

int test(int a = 0,int b=0,int c)
{
	cout << a<<b<<c;
}
传参时,必须从左往右给,不能隔着给

test(1,2,3);
test(1,2);
test(1);
test();
int main() 
{
	test();//这里没有传参,那么函数里,a就是0
	test(10,20,30);//这里传参了,那么函数里,a就是传的值
	return 0;
}
缺省参数也分全缺省和半缺省

上面的是全缺省

下面是半缺省

int test(int a ,int b=0,int c=1)
{
	cout << a<<b<<c;
}
注意,半缺省,只能从右往左给,必须连续

test(1);
test(1,2);
test(1,2,3);


注意,如果在声明函数和定义函数时都给了缺省参数,那么编译器就不知道究竟是哪个了
也不能只在定义位置给,因为如果别人调用了头文件的声明部分呢
所以最后只能在声明的地方给缺省参数

?4.函数重载

函数重载类似解决重名问题,但重点是在同一作用域里,我们前面采用命名空间,是可以引用到不同的作用域里的函数、变量。

但如果在同一个作用域,那么重名的函数怎么办呢,c语言不支持同一作用域里的重名函数,

c++就支持了重名函数(参数不同、参数个数不同、参数顺序不同之一),返回值没有要求,不同返回值还是代表同样的函数,因为在传参时,编译器是对参数进行匹配,从而确定是哪个函数的

int as(int a, int b, int c)
{
	return 1;

}
int as(int a, int b)
{
	return 1;

}
int as(int a, int b, double c)
{
	return 1;

}
int as(double a, double b, double c)
{
	return 1;
}
int as(double c,int a, int b)
{
	return 1;

}


int main()
{
	as(2, 3, 4);
	as(2, 3);
	as(1, 2, 3.1);
	as(2.1, 1.1, 3.1);
    as(2.1,3, 3);
	return 0;
}
注意缺省参数与没有缺省参数,也构成了重载
参数重载还有很多类型,我们可以私下尝试下,结合报错信息
就可以看到很多种不同类型的函数重载

4.1编译器原理(部分)

我们先简单梳理下,以便待会解释,为什么c语言不支持重载而c++重载

编译器的还有一些内容,我在c语言的文章里有写。

假如此时:func.h(包含了函数的声明),func.c(包含了函数的定义和.h文件的引用),test.c(包含了调用函数和.h文件的引用)文件

接下来,当我们启动程序,编译器先进行预处理,生成func.i(包含了函数声明和定义),test.i(函数的声明和实际调用),

接下来,我们进行编译,此时,编译器并没有找到函数的真实地址,只是通过声明,暂时放了个调用在上面。

再接下来是汇编,将代码变成二进制代码,最后通过链接,链接整个工程文件,此时因为func.c文件有函数的定义,最终编译器可以成功将暂时放的调用代码替换成真实的函数地址,完成编译。

接下来解释c语言不支持重载的原因,其实就是c语言调用函数,是通过函数名找地址,但c++中,找函数时,是将函数的参数也放进去,这样就能更加精确找到函数。

5.引用

?引用就是给变量和函数起别名

	int a = 1;
	int& b = a;
	int& c = a;
	int& d = c;
	cout << a;
	cout << b;
	cout << c;
	cout << d;
(跟define和typedef不一样)
因为,define是简单的文字替换
typedef重新定义类型
而引用是创建一个引用变量,在上面的例子里面
可以看见,a、b、c、d都是共用一个空间,可以理解为一个
内存空间,有多个名称,因此这几个变量的地址都是相同的

一个变量可以有多个引用,引用变量和引用实体都必须是同一个类型
引用变量必须初始化

引用一旦引用了一个实体,不能再引用别的实体
如果引用实体是个常量
要加const
const int&a=3;


本质上,常引用的宗旨是,权限不能放大,但可以放小
const int a=1;
int &b=a//这是权限放大,错误
int a=1;
const int&b=a;//权限缩小,可以

int i=1;
double b=i;//赋值操作
//这里会进行隐式转换类型
//所以会创建一个临时变量

double &rb=i//错误,因为赋值语句,是会创建临时变量
//比如上面的上面,i的值会赋给临时变量,临时变量的值再拷贝给b
//而这里也是一样,但问题是,临时变量本质上是常量,或者是临时常量
//所以,可以这样写
const double&rb=i//这样就可以了

5..1用途

5.1.1用途1:传参

void Swap(int& l, int& r)
{
	int tmp = l;
	l = r;
	r = tmp;
}
int main()
{
	int a = 1;
	int& b = a;
	int& c = a;
	int& d = c;
	cout << a;
	cout << b;
	cout << c;
	cout << d;
	int h = 2;
	int& j = h;
	Swap(a, j);
	cout << a;
	return 0;
}

乍一看好像跟指针区别不大,
但参考我c语言版链表文章中,单链表插入时,我们需要使用
2级指针,但有了这个,我们就可以直接用引用写

注意,c++的引用不能二次改变指向的引用实体,这样使得指针不能被抛弃,其他语言如java的引用是可以二次改变指向引用实体。

5.1.2返回值

首先,我们理解这块代码,
int add(int a, int b)
{
	a++;
	return a;
}
这个函数就是传统的传值返回,函数
栈在结束函数调用后销毁,销毁前把a的值
拷贝到寄存器或其他一块空间,最后再把值拷贝回下面的c
int& add2(int a, int b)
{
	a++;
	return a;
}
这是传引用,但事实上,这里的代码是很危险的
因为,传引用,意味着,是将a的别名,返回给b
,本质上还是把a所处的空间的返回了,而a是局部变量,函数调用结束
这块空间名义上是非法的,如果期间有别的操作,很可能使这块空间
被覆盖,使得值变成不确定的值。

int main()
{
	int c = add(3, 4);
	int b = add2(3, 4);
    int&d = add2(3, 4);
	cout << c << endl << b;
	return 0;
}
注意,引用返回,针对的是在函数调用后不会销毁的变量等。
比如静态变量,全局变量

引用返回和传值返回,效率差距比较大,尤其是返回的数据很大时,传值返回时,要额外一个临时变量来存储返回值,再把临时变量的值拷贝给函数调用时赋值的对象。

5.2引用和指针

在语法上,引用是不开辟额外空间的,指针开辟额外空间,底层上,两者都是开辟额外空间。但一般,我们默认把引用认为是不开辟额外空间的,都是把引用认为是别名。

1.引用概念上就是定义一个变量的别名,指针存储一个指针地址

2.引用必须初始化,指针没有要求

3.引用不可以二次改变指向的变量,指针可以任意改变

4.没有NULL引用,但有NULL指针

5.sizeof针对引用,计算的是引用的引用实体大小,计算指针是计算指针变量本身占据空间的大小(比如32位的4字节)

6.引用自加,引用实体也会+1,指针自加,只会按当前指针指向的类型,后跳相应字节数。

7.有多级指针,但没有多级引用

8.引用调用时,不需要额外加解引用,编译器自己会处理,指针要自己加解引用

9.引用相对指针比较安全

6.内联函数

?c语言有宏函数,可以通过替换文本的方式,设计计算,但对复杂计算很容易出事,比如优先级问题,类型安全问题等,所以c++加了内联函数,通过inline关键字,可以让函数调用变成变成函数展开,同样可以减少开辟函数空间的行为,并且可以克服类型安全、优先级等问题。而且内联函数可以调试。

inline int add(int a, int b)
{
	a++;
	return a;
}


int main()
{
	int c = add(3, 4);

	return 0;
}
注意,内联函数本质也是展开,那么如果函数的代码量很大,
并且调用次数多,那么整个程序的代码量就会飞升
inline对编译器来说,只是一个建议,编译器可以选择忽略inline特性
一般对函数规模小的,编译器会受理,对递归、规模大的函数,编译器可以选择忽略

注意,内联函数没有地址,因为内联函数会被展开,这时候,编译器不会寻找它的地址,那么如果声明和定义在两个文件,那么最后整个程序运行时,编译器就会报错,因为在调用函数的位置上,被展开的是声明,但声明本身不能被当做函数使用,需要定义才可以,所以才会报错。

因此通常声明和定义不分离对于内联函数。

7.auto

?auto是用来简化很长的类型名,具体可以看我后面关于stl等方面的文章。

auto本质上让编译器通过赋值的内容,自动推到变量的类型。

	int a = 1;
	auto c = 1;
    //auto j;是错误的,因为没有赋值,编译器不能推导类型,而上面的
    //的表达式因为有赋值,就可以推导类型。
	auto* c = &a;
	auto d = &a;
    //auto*和auto没有区别,类型都会被识别为地址
	auto& b = a;
    //如果要用引用,必须要&符号,因为不加,编译器会认为是一般的情况,而不是引用
	auto g = 3, h = 1;
    //同行多个变量,必须保证类型相同,编译器会根据第一个值的类型,给后面的
    //变量替换类型。


    auto不能作为形参的类型,也不能作为返回值。
    
    auto也不能用来定义数组

8.范围for

?

	int h[3] = { 0,2,3 };
	for (int e : h)
	{
		cout << e;
	}
	for (int& e : h)
	{
		e *= 3;
	}
范围for,不需要我们自己计算数组究竟有多大,会自动判断范围
自动从下标低到高。
第二个for是通过引用的方式,改变数组自身的值

注意,范围for必须有明确的数组范围

int ao(int h[])
{
	for (int e : h)
	{
		//,...
	}
}
这样就是错误写法,因为不明确范围。
迭代的对象要能++和==,具体之后的文章里我会再说

9.空指针

?在c的头文件中,关于NULL,是定义成0常量或者(void*)0,但编译器一般都是识别为0常量,但由于0在地址里也是空,所以用NULL初始化指针也不会出错,但如果遇到了要分开的时候。

int ao(int a)
{

}
int ao(int* a)
{

}


int main()
{
	ao(0);
	ao(NULL);
	return 0;
}
对于这两个函数的调用,编译器在识别函数的时候,因为默认是常量0
所以,这两次调用,实际上调用的都是形参是int a的函数
所以c++11里面引入了新的关键字nullptr
nullptr等同于(void*)0,所占空间大小也是一样的

文章来源:https://blog.csdn.net/MXZSL/article/details/135223963
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。