C++第一讲:开篇

C++第一讲:开篇

这一讲是学习C++的第一讲,以后会一直学习C++,后序所写的算法题也将会使用C++来写,所以这一讲叫做开篇,讲述的是C++的基础知识

1.C++历史背景

关于C++的历史背景,了解与否其实无关紧要,但是这里还是稍微讲讲有意思的事情吧,毕竟学习语言首先还是要先了解一下它的历史

1.1C++创世主--本贾尼

C++的起源可以追溯到1979年,当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不同的地⽅可能有差异)在⻉尔实验室从事计算机科学和软件⼯程的研究⼯作。他在实现项目的开发工作时,感受到了C语言在一些方面的不足,1983年,于是他在C语言之上添加了面向对象的特性,也是在这一年该语言被命名为C++

仰望大佬:

1.2C++版本更新

1.3C++的重要性

C++的重要性想必也不用过多赘述了,学习C++的人应该都能够体会到C++的重要性,无论是在工作中、学习中

1.4C++书籍推荐

下面的三本书是大佬推荐的书,至于怎么样,我相信,到后边我会看的!

2.C++的第一个程序

c 复制代码
//第一个C++程序
#include <iostream>
using namespace std;

int main()
{
	cout << "hello world!" << endl;

	return 0;
}

这里我们是不是还看不懂这些代码?没关系,我们往下看

3.命名空间

3.1namespace是什么

在C语言中,我们不能使用同一个名称定义多个变量:

c 复制代码
#include <stdio.h>
//在只引用stdio这一个头文件的情况下,这串代码是正确的
int rand = 10;

int main()
{
	printf("%d ", rand);

	return 0;
}

那么下面我们改一下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
//在引用stdlib头文件之后,就发生了报错:rand重定义,以前的定义是"函数"
//原因:引用头文件其实就是在链接过程将头文件内容拿过来,而stdlib这一头文件中
//      也包含rand,而且它还是一个函数,所以这里就报错了
int rand = 10;

int main()
{
	printf("%d ", rand);

	return 0;
}

这在以后多人项目中是非常拉跨的,因为我们不能保证整个团队定义的变量都是不同的名称,所以C++中就引入了namespace来解决这个问题

3.2namespace的使用

总结:

1.namespace是定义命名空间的关键字,命名空间中可以包含变量、函数、结构体等

2.在C++中,两个冒号::被称为是作用域解析运算符,它用来指定某个实体(变量、函数、类)属于哪个作用域或命名空间

3.namespace本质就是定义了一个命名空间,这个域跟全局域相互独立,不同的域可以定义同名变量,所以这里的rand就不会报错了

4.C++中有函数局部域、全局域、命名空间域、类域,编译器在编译时会先在局部域中寻找变量,然后再在全局域中寻找变量。局部域和全局域除了会影响编译查找逻辑,还会影响变量的声明周期,命名空间域和类域不会影响变量声明周期
1.namespace是定义命名空间的关键字,命名空间中可以包含变量、函数、结构体等:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
//namespace定义命名空间,定义方法如下:
//namespace+命名空间名称--自己起--BCNR--爆炒脑仁
namespace BCNR
{
	//定义变量
	int rand = 10;

	//定义函数
	int ADD(int x, int y)
	{
		return x + y;
	}

	//定义结构体
	struct Stack
	{
		int* arr;
		int capacity;
		int top;
	};
}

int main()
{
	printf("%p\n", rand);

	//使用命名空间中对象的方法:命名空间名称 + :: + 对象名称
	//提取变量
	printf("%d\n", BCNR::rand);
	//使用空间中的函数
	int c = BCNR::ADD(10, 20);
	printf("%d\n", c);
	//使用结构体
	BCNR::Stack st1;
	return 0;
}

2.在C++中,两个冒号::被称为是作用域解析运算符,它用来指定某个实体(变量、函数、类)属于哪个作用域或命名空间
3.namespace本质就是定义了一个命名空间,这个域跟全局域相互独立,不同的域可以定义同名变量,所以这里的rand就不会报错了
4.C++中有函数局部域、全局域、命名空间域、类域,编译器在编译时会先在局部域中寻找变量,然后再在全局域中寻找变量。局部域和全局域除了会影响编译查找逻辑,还会影响变量的声明周期,命名空间域和类域不会影响变量声明周期

c 复制代码
#include <stdio.h>
//定义在全局域
int a = 10;

namespace BCNR
{
	//定义在命名空间域
	int a = 20;
}

//函数局部域
int ADD(int x, int y)
{
	return x + y;
}

int main()
{
	//定义在函数局部域
	int a = 30;

	printf("%d\n", a);//main函数局部域中,30
	printf("%d\n", BCNR::a);//命名空间域中,20
	printf("%d\n", ::a);//像这样只有两个冒号,就是访问全局域中的变量,10
	return 0;
}

3.3namespace使用注意事项

总结:

1.namespace可以嵌套使用

2.namespace只能定义在全局

3.项目工程中定义的多个同名namespace会认为是一个namespace,不会冲突

4.C++标准库都放在一个叫std(standard)的命名空间中

5.namespace创建的命名空间中的对象其实都是全局的,因为这个变量能够在非命名空间域中被访问
1.namespace可以嵌套使用:

c 复制代码
#include <stdio.h>
//假设此时张三和李四要实现一个项目:Game
namespace Game
{
	//张三
	namespace ZhangSan
	{
		int a = 10;
		int ADD(int x, int y)
		{
			return x + y;
		}
	}

	//李四
	namespace LiSi
	{
		int a = 20;
		int ADD(int x, int y)
		{
			return x + y * 10;
		}
	}
}

int main()
{
	int ret;

	//张三
	printf("%d\n", Game::ZhangSan::a);//10
	ret = Game::ZhangSan::ADD(10, 10);
	printf("%d\n", ret);//10+10=20

	//李四
	printf("%d\n", Game::LiSi::a);//20
	ret = Game::LiSi::ADD(10, 10);
	printf("%d\n", ret);//10+10*10=110
	return 0;
}

2.namespace只能定义在全局
3.项目工程中定义的多个同名namespace会认为是一个namespace,不会冲突

c 复制代码
//同名namespace会合并在一起
//Stack.h
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{
	typedef int STDataType;
	typedef struct Stack
	{
		STDataType* a;
		int top;
		int capacity;
	}ST;

	void STInit(ST* ps, int n);
	void STDestroy(ST* ps);
	void STPush(ST* ps, STDataType x);
	void STPop(ST* ps);
	STDataType STTop(ST* ps);
	int STSize(ST* ps);
	bool STEmpty(ST* ps);
}

// Stack.cpp
#include"Stack.h"
namespace bit
{
	void STInit(ST* ps, int n)
	{
		assert(ps);
		ps->a = (STDataType*)malloc(n * sizeof(STDataType));
		ps->top = 0;
		ps->capacity = n;
	}
	// 栈顶
	void STPush(ST* ps, STDataType x)
	{
		assert(ps);
		// 满了, 扩容
		if (ps->top == ps->capacity)
		{
			printf("扩容\n");
			int newcapacity = ps->capacity == 0 ? 4 : ps->capacity
				* 2;
			STDataType* tmp = (STDataType*)realloc(ps->a,
				newcapacity * sizeof(STDataType));
			if (tmp == NULL)
			{
				perror("realloc fail");
				return;
			}
			ps->a = tmp;
			ps->capacity = newcapacity;
		}
		ps->a[ps->top] = x;
		ps->top++;
	}
}

4.C++标准库都放在一个叫std(standard)的命名空间中

c 复制代码
#include <iostream>

int main()
{
	//既然C++标准库都放在了std这一命名空间中,那么我们在引用了
	//头文件之后就可以对其中的对象进行使用了:
	std::cout << "hello world!" << std::endl;

	return 0;
}

5.namespace创建的命名空间中的对象其实都是全局的,因为这个变量能够在非命名空间域中被访问

3.4命名空间的使用

上述我们已经了解到,双冒号可以拿出指定命名空间中的变量来使用,如果我们要一直拿出某个变量的话,敲代码是一件很麻烦的事,所以我们这样做:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
namespace BCNR
{
	//定义变量
	int a = 10;

	//定义函数
	int ADD(int x, int y)
	{
		return x + y;
	}
}

//如果我们要像下面多次使用a的话,是很麻烦的,那么我们加上这个:
//此时代表着展开BCNR这一命名空间中的a变量,将a变量暴漏在全局
using BCNR::a;
//这样代表着将命名空间中所有的对象暴漏在全局中
using namespace BCNR;

int main()
{
	printf("%d\n", a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	printf("%d\n", BCNR::a);
	return 0;
}

现在我们就可以理解C++的第一个代码中的using namespace std是什么意思了

4.C++输入和输出

1.iostream是标准输入流和标准输出流的一部分,它提供了对标准输入和标准输出的访问,是istream和ostream的组合,istream表示输入流,用于从输入流(如键盘)中读取数据,ostream为输出流,用于向输出流(如屏幕)中输出数据

2.std::cin,是istream类的对象,用于从标准输入流中读取数据

3.std::cout,是ostream类的对象,用于向标准输出流发送数据

4.std::enl,它是一个函数,而且是一个较为复杂的函数,相当于插入一个换行符并刷新缓冲区

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

6.与C语言不同的是,C++中的输入输出可以自动识别数据类型,不需要使用%d、%c来格式化输入、输出

7.IO流中设计很多类和对象、重载中的知识,后边会详细阐述

8.cout、cin、endl等都属于C++标准库,C++标准库都放在一个叫std的命名空间中,所以要通过命名空间的使用方法来使用它们

9.日常练习中,我们可以用using namespace std,在项目中万不可这样使用

10.仅仅包含iostream,我们也可以使用printf、scanf,可能是在iostream中间接包含了,VS没有报错,其他编译器可能会报错,这也说明了IO流不仅仅是各司其职的,在IO需求较高的情况下,比如竞赛中,可能会因为IO流之间的关系处理而浪费时间,解决方式如下:

c 复制代码
#include <iostream>
int main()
{
	int a = 0;
	double b = 0.1;
	char c = 'x';

	//会自动识别数据类型进行打印
	//如果想要保留小数的话,建议使用printf来实现小数的保留
	std::cout << a << " " << b << " " << c << std::endl;

	//可以⾃动识别变量的类型
	std::cin >> a;
	std::cin >> b >> c;
	std::cout << a << std::endl;
	std::cout << b << " " << c << std::endl;
	return 0;
}

#include<iostream>
using namespace std;
int main()
{
	//在IO需求⽐较⾼的地⽅,如部分⼤量输入的竞赛题中,加上以下3⾏代码
	//可以提⾼C++IO效率
	ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	return 0;
}

5.缺省参数

缺省参数其实就是在函数声明时,为函数参数定义一个缺省值,如果传入的数据没有值的话,就会使用缺省值:

c 复制代码
#include <iostream>
using namespace std;

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

int main()
{
	Func(); //没有传参时,使⽤参数的默认值,输出0
	Func(10); //传参时,使⽤指定的实参,输出10
	return 0;
}

缺省分为全缺省和半缺省

c 复制代码
#include <iostream>
using namespace std;

// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

// 半缺省
//半缺省中的参数必须从右向左进行赋值
//void Func2(int a = 10, int b, int c = 20)//err,缺少实参2的默认实参
void Func2(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}

int main()
{
	Func1();
	Func1(1);
	Func1(1, 2);
	//在传参时也不能跳跃着传参
	//Func1(, 1, );//err,语法错误
	Func1(1, 2, 3);

	Func2(100);
	Func2(100, 200);
	Func2(100, 200, 300);
	return 0;
}

需要注意的是,缺省参数不能声明和函数同时给值,只能够在声明中确定缺省值

c 复制代码
//Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
	STDataType* a;
	int top;
	int capacity;
}ST;

//只能在这里给缺省值
void STInit(ST* ps, int n = 4);


//Stack.cpp
#include"Stack.h"
//缺省参数不能声明和定义同时给,这里就不能给缺省值了
void STInit(ST* ps, int n)
{
	assert(ps && n > 0);
	ps->a = (STDataType*)malloc(n * sizeof(STDataType));
	ps->top = 0;
	ps->capacity = n;
}

6.函数重载

C++支持在同一作用域中出现同名函数,但是这要求函数的形参不同,可以是形参的个数不同,也可以是形参的类型不同:

c 复制代码
#include<iostream>
using namespace std;
//1、参数类型不同
int Add(int left, int right)
{
	cout << "int Add(int left, int right)" << endl;
	return left + right;
}
double Add(double left, double right)
{
	cout << "double Add(double left, double right)" << endl;
	return left + right;
}
//2、参数个数不同
void f()
{
	cout << "f()" << endl;
}
void f(int a)
{
	cout << "f(int a)" << endl;
}
//3、参数类型顺序不同(本质上也是形参类型不同)
void f(int a, char b)
{
	cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
	cout << "f(char b, int a)" << endl;
}

int main()
{
	Add(10, 20);
	Add(10.1, 20.2);

	f();
	f(10);

	f(10, 'a');
	f('a', 10);
	return 0;
}

返回值不同不能作为重载条件,因为调用时也无法区分

c 复制代码
//返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{}

int fxx()
{
 	return 0;
}

注意点:

c 复制代码
//下⾯两个函数构成重载
//但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{
	cout << "f()" << endl;
}

void f1(int a = 10)
{
	cout << "f(int a)" << endl;
}

int main()
{
	//err,对重载函数的调用不明确
	f1();

	return 0;
}

7.引用

7.1什么是引用

引用其实就是给变量起别名,引用并不是新定义了一个变量,编译器不会给引用变量开辟空间

7.2引用的定义

cpp 复制代码
//引用的定义
类型& + 引用别名 = 引用对象

引用的使用如下:

cpp 复制代码
//类型 & +引用别名 = 引用对象
int main()
{
	//定义一个整形变量
	int a = 10;

	//为整形变量起别名
	int& ra = a;

	//为别名起别名
	int& rra = ra;

	//为同一个整形变量起多个别名
	int& rb = a;

	//它们指向的地址相同,说明并没有为它们单独开辟空间
	cout << &a << endl;
	cout << &ra << endl;
	cout << &rra << endl;
	cout << &rb << endl;
	return 0;
}

而引用的原理为:

对变量赋值和对变量引用的区别:

7.3引用的使用

因为引用对象和原对象指向的空间相同,所以说,对引用对象进行修改,原对象也会被修改,引用的底层实现其实是指针,所以我们可以这样写代码:

c 复制代码
//引用的使用
//对引用对象的修改会直接影响到原来的对象
//所以这里交换就会成功
void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int x = 10;
	int y = 20;
	//10 20
	cout << x << " " << y << endl;

	Swap(x, y);

	//20 10
	cout << x << " " << y << endl;
	return 0;
}

Swap函数之前使用指针也能够完成,那么有了指针,引用是不是多余了呢?肯定不是,我们看下边的一个例子:

cpp 复制代码
//假设此时我们已经创建了一个栈
//void STInit(ST& rs, int n = 4);栈的初始化
//void STPush(ST& rs, STDataType x);入栈
//int& STTop(ST& rs);//获取栈顶元素
int main()
{
	ST st1;

	STInit(st1);
	STPush(st1, 1);
	STPush(st1, 2);

	//此时我们要获取栈顶元素:2
	cout << STTop(st1) << endl;
	return 0;
}

获取栈顶元素比较简单,但是这时我们想要对栈顶元素进行修改,方法如下:

c 复制代码
// int STTop(ST& rs)
//如果要对栈顶元素进行修改,只需要将返回类型改为引用即可
//此时返回的是对原始对象的引用,而不是对象的副本(如果返回值的话,返回时需要将值先存储到一个临时空间中,所以是对象的副本)
int& STTop(ST& rs)
{
	return rs.a[rs.top-1];
}

//假设此时我们已经创建了一个栈
//void STInit(ST& rs, int n = 4);栈的初始化
//void STPush(ST& rs, STDataType x);入栈
//int& STTop(ST& rs);//获取栈顶元素
int main()
{
	ST st1;

	STInit(st1);
	STPush(st1, 1);
	STPush(st1, 2);

	//此时我们要获取栈顶元素:2
	cout << STTop(st1) << endl;

	//此时我们要读栈顶元素进行修改:
	STTop(st1)++;
	cout << STTop(st1) << endl;//输出3
	return 0;
}

这时如果要用指针来实现的话是非常麻烦的,感兴趣的可以实现一下!

7.4引用注意事项

1.由于引用作为返回值或传参不需要开辟临时变量,所以可以提效,而且还可以通过引用直接修改到原变量,所以能够用引用的地方使用引用比指针更加方便

2.引用和指针在实践中是相辅相成的,功能有重叠性,各有特点,互相不可替代,但是与Java中的引用不同,C++中的引用不能够改变指向

3.引用可以简化程序,比如使用引用代替指针传参,可以避免复杂的指针,更容易理解

4.引用必须初始化

5.引用一旦引用一个实体,就不能引用其他实体

6.引用时如果引用临时变量,就可能出现野引用的情况

c 复制代码
int main()
{
	int a = 10;
	int b = 20;

	//5.引用一旦引用一个实体,就不能引用其他实体:
	int& ra = a;
	//&ra = b;  err:无法从int转换成int*
	ra = b;//这里是赋值,此时a = ra = b = 20

	//4.引用必须初始化
	//int& rb;  err:必须初始化引用
	return 0;
}
cpp 复制代码
//野引用
int& f()
{
	int a = 10;

	return a;
}

7.5const引用

总的来说,说的其实就是权力的问题,权力可以缩小,但是不能扩大

7.5.1示例1--权力扩大问题

cpp 复制代码
//const引用示例1
int main()
{
	const int a = 10;//此时a为const类型
	//int& ra = a;//err,权限不能扩大,不能从const int转换为int&

	const int& ra = a;//√,const int&和const int权限相同
	return 0;
}

7.5.2示例2--权力缩小

cpp 复制代码
//const引用示例2
int main()
{
	int a = 10;

	const int& ra = a;//权力可以缩小
	int& rb = a;//正确
	int& rra = ra;//err,无法从const转换成int&
	return 0;
}

7.5.3示例3

cpp 复制代码
int main()
{
	int a = 10;

	int& rb = 30;//err,因为30是常数
	const int& rrb = 30;//这个才是正确的写法

	int& ra = a*3;//err,因为a*3是常数
	const int& rra = a*3;//这样也是正确的,表示a*3被rra引用,可以通过rra找到a*3,但是不能改变a*3的值
	rra = 20;//err,不能给常量赋值

	double c = 1.2;
	const int& rc = c;//虽然是int类型,但是这样也是对的,涉及到整形提升
	//这里就是先将1.2传到一个int类型的临时变量中,然后再让rc进行指向
	return 0;
}

7.6指针与引用之间的关系

C++中的指针和引用就像两个性格不同的亲兄弟,指针是哥哥,引用为弟弟,它们在实践中相辅相成,但各自有各自的特点,互相不可替代

1.语法概念上,引用是为变量起别名,不需要开辟空间,指针存储的是一个变量的地址,需要开辟空间

2.sizeof引用的大小是引用对象的类型发=大小,而sizeof指针始终是4/8个字节

3.引用在定义时必须初始化,指针只是建议初始化

4.引用在引用一个对象后,就不能引用其它对象,而指针可以不停地改变指向

5.引用可以直接访问引用对象,而指针需要解引用

6.指针很容易出现野指针情况,而引用比较难以出现野引用的情乱

8.inline

8.1define定义宏的劣势

之前我们就已经讲过,define定义的宏具有很大的劣势,比如我们要定义一个宏函数:

c 复制代码
//错误写法1:
#define ADD(int x,, int y) return x+y
//错误写法2:
#define ADD(int x, int y) (x+y)
//错误写法3:
#define ADD(x, y) (x+y)
//正确写法:
#define ADD(x, y) ((x) + (y))
//1.形参中不用追加类型
//2.必须加上合适的小括号
//3.最后边不能加;因为是宏替换
//4.外面不加括号:x=1+2, y=2+3形成错误
//5.里边不加括号:x=1|2,y=2^3形成错误,位移运算符优先级低于+

为了解决这一个问题,C++中设计了一个inline函数:

8.2inline函数的使用

cpp 复制代码
//inline函数的使用
//直接在函数前加上关键词inline即可
inline int ADD(int x, int y)
{
	return x + y;
}

int main()
{
	cout << ADD(10, 20) << endl;//输出30

	return 0;
}

8.3inline函数使用注意事项

总结:

1.inline修饰的函数被称为内联函数,在使用内联函数时,不需要为改函数创建栈帧,可以提高效率

2.inline对于编译器来说只是建议,也就是说,内敛函数的展开与否取决于编译器,一般内联函数短小时就会展开,内联函数较大时就不会展开

3.inline不建议声明和定义分离到两个文件,分离会导致链接错误,因为inline被展开,是没有函数的地址的,链接时就会报错

4.VS中debug版本下是默认不展开的,想要展开可以搜索着改动一下编译器

9.nullptr

NULL其实是一个宏,在传统的C头文件中,可以看到:

cpp 复制代码
#ifndef NULL
	#ifdef __cplusplus
		#define NULL 0
	#else
		#define NULL ((void *)0)
	#endif
#endif

也就是说,C++中,NULL可能被定义为字面量0,或者在C中被定义为(void*)的常量,这样在使用重载的函数时,就会遇到问题:

cpp 复制代码
//两个重载函数:
void f(int x)
{
	cout << "f(int x)" << endl;
}

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

int main()
{
	f(0);

	f(NULL); //这里调用的也是f(int x)函数,因为NULL被解析成了0
	f((int*)NULL); //编译报错:error C2665: "f": 2 个重载中没有⼀个可以转换所有参数类型f((void*)NULL);
	return 0;
}

既然有着这样的问题,那么就设计出一个新的东西:nullptr

nullptr是一个特殊的关键字,是一种特殊类型的字面量,它可以转换成其它任意类型的指针类型,nullptr只能被转换成指针类型,而不能被转换成整数类型,所以以后用nullptr千万不要陌生!

相关推荐
敲代码的奥豆16 分钟前
C++:日期类的实现
开发语言·c++
技术无疆25 分钟前
ButterKnife:Android视图绑定的简化专家
android·java·android studio·android-studio·androidx·butterknife·视图绑定
看山还是山,看水还是。29 分钟前
c#进度条实现方法
c语言·开发语言·笔记·c#
孑么32 分钟前
C# 委托与事件 观察者模式
开发语言·unity·c#·游戏引擎·游戏程序
ZachOn1y34 分钟前
Java 入门指南:JVM(Java虚拟机)垃圾回收机制 —— 垃圾收集器
java·jvm·后端·java-ee·团队开发·个人开发
敲代码不忘补水41 分钟前
Python Pickle 与 JSON 序列化详解:存储、反序列化与对比
开发语言·python·json
攸攸太上1 小时前
Java通配符的作用
java·学习·通配符
蜡笔小新星1 小时前
机器学习和深度学习的区别
开发语言·人工智能·经验分享·深度学习·学习·机器学习
齐 飞1 小时前
使用jackson将xml和对象、List相互转换
xml·java·spring boot·后端·list
liwulin05061 小时前
java-在ANTLR中BaseListner的方法和词法规则的关系0.5.0
java·开发语言