c++基础

目录

1.C++关键字

2.命名空间

2.1 命名空间定义

2.2 命名空间使用

3.C++输入&输出

4.缺省参数

4.1 缺省参数概念

4.2缺省参数分类

5.函数重载

5.1函数重载概念

5.2C++支持函数重载的原理--名字修饰

6.引用

6.1引用概念

6.2引用特性

6.3常引用

6.4使用场景

6.5传值、传引用效率比较

6.6引用和指针的区别

7.内联函数

7.1概念

7.2特性

8.auto关键字

8.1 类型别名思考

8.2 auto简介

8.3 auto的使用细则

8.4 auto不能推导的场景

9.基于范围的for循环

9.1 范围for的语法

9.2 范围for的使用条件

10.指针空值--nullptr

10.1 C++98中的指针空值


1.C++关键字

C++总计63个关键字,C语言32个关键字
ps:下面

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast

常见重要关键字

①auto

关于auto在下文有说到。

②inline

关于inline,在下文的内联函数中有说到。

③sizeof

sizeof是运算符,测量的是字符的大小,会包括'\0'。

①测量数组时,返回的是分配的数组空间大小
②测量指针时,返回的是改指针的大小,一般为4/8
③测量类型时,返回的是这个类型所占的空间大小
④测量对象时,返回的是对象占用的空间大小
⑤测量函数时,返回的是函数返回类型所占空间的大小。

strlen是C语言标准库的函数,测量的是字符串的有效实际长度,以'\0'结束,不包括'\0'。如果测量的字符串没有给出'\0',strlen会是一个随机值。

char *str1=“abcde”;
char str2[]=“abcde”;
char str3[5]={‘a’};
int str4[5]={‘a’};
char str5[] = “0123456789”;

输出:

sizeof(str1); // 4 计算的是指针内存的大小,包括’\0’
sizeof(str2); // 6  计算的是字符串的内存大小,包括’\0’
sizeof(str3); // 5  计算的是char型数组的内存大小
sizeof(str4); // 20  计算的是int型数组的内存大小
sizeof(str5); // 11  计算的是字符串的大小,包括’\0’
strlen(str1); // 5  计算的是字符串长度,不包括‘\0’
strlen(str2); // 5  计算的是字符串长度,不包括‘\0’
strlen(str3); // ? str3数组中没有给出'\0',而strlen是以'\0'结束的,因此会是一个随机值
strlen(str4); // ? str4数组中没有给出'\0',而strlen是以'\0'结束的,因此会是一个随机值
strlen(str5); // 10  计算出字符串有效实际长度,但是不包括‘\0’

④static

static主要用于修饰变量和函数,被修饰的变量和函数会变成静态变量或静态函数。

static用法:

修饰局部变量---被修饰的局部变量称为静态局部变量

被修饰的变量会从栈区改变到静态区,此时的变量不会随某个函数的消耗而消耗,而是只有程序结束了才会被消耗。static修饰局部变量只改变生命周期,不改变作用域!

void test()
{
	static int x = 0;
	x++;
	printf("%d ", x);
}

int main()
{
	int i = 0;
	while (i < 10)
	{
		test();//每循环一次都会调用一次test()。x打印的值为:1 2 3 4 5 6 7 8 9 10
		i++;
	}
	//static修饰局部变量只改变生命周期,不改变作用域!
	//printf("%d ", x);
	return 0;
}

修饰全局变量--被修饰的全局变量称为静态全局变量

被修饰的全局变量没有改变其存储的区域,全局变量和静态全局变量都是存储在静态区。被改变的是全局变量的链接属性,改变了全局变量的作用域。普通全局变量,是可以被外部的源文件引用,只要需要引用这个全局变量的那个源文件内,使用关键字extern来声明外部符号,即可引用。而使用static,称为静态全局变量后,是不可以了。

因此,总结一下就是全局变量本身是具有外部链接属性的,在A文件中定义的全局变量,在B文件中可以通过外部链接来使用,但如果全局变量被static修饰,那这个外部链接属性就会被修改成内部链接属性,此时这个全局变量就只能在自己的源文件中使用。

修饰函数--被修饰的函数称为静态函数

静态函数与静态全局变量类似,普通函数具有外部链接的属性,作用域可以在本源文件中,其它源文件也可以通过extern关键字调用之。

顺便说说外部链接和内部链接:

内部链接(internal linkage):

内部链接意味着变量或函数只在声明它们的源文件内可见。变量或函数被定义为具有内部链接时,只能在定义它们的源文件中访问它们。这意味着在其他源文件中无法通过extern关键字进行访问。对于全局变量来说,如果没有显式地使用static关键字来指定内部链接,则默认具有内部链接属性。

例如,以下代码中的变量"int x"和函数"static void func()"都具有内部链接属性:

// file1.c
static int x;
static void func() {
    // 内部链接变量和函数的定义
}

外部链接(external linkage)

外部链接意味着变量或函数在整个程序中可见,可以在多个源文件之间共享和访问。变量或函数被定义为具有外部链接时,可以在其他源文件中使用extern关键字来引用和访问它们。

全局变量默认具有外部链接属性,除非使用static关键字显式指定为内部链接。例如,以下代码中的变量"int y"具有外部链接属性: 

// file1.c
int y; // 具有外部链接的全局变量

 ⑤entern

extern是一个用于声明全局变量或函数的关键字,它告诉编译器该变量或函数在其他源文件中已经定义或声明过。通过使用 "extern" 关键字,我们可以在一个源文件中引用其他源文件中定义的全局变量或函数。

当我们在一个源文件中使用 "extern" 关键字来声明一个全局变量时,编译器会知道该变量是在其他源文件中定义的,并会在链接阶段将其连接起来。

例如,假设有两个源文件 "file1.c" 和 "file2.c",并且在 "file1.c" 中定义了一个全局变量 "int x"。如果我们想在 "file2.c" 中使用该全局变量,我们可以在 "file2.c" 中使用 "extern" 关键字来声明它。

⑥const

const关键字和宏#define

const和#define都可以用来定义常量,但它们在语法上和使用上存在一些区别:

作用方式:
const:const是C/C++语言中的关键字,用于定义一个变量,并指定该变量的值不能被修改。常量变量在编译时分配内存,并具有类型安全检查。
#define:#define是C/C++的预处理指令,用于执行简单的文本替换。它不会在预处理阶段检查语法或类型,而只是在编译前进行文本替换。

类型安全:
const:const定义的常量是有类型的,并且具有类型安全性。编译器可以进行类型检查,保证常量的值与所定义的类型匹配。
#define:#define定义的常量是纯文本替换,没有类型信息,无法进行类型检查,容易导致潜在的错误。

作用域:
const:const定义的常量具有块作用域,只在定义它的块内可见,且不会污染命名空间。
#define:#define定义的常量是全局的,可以在整个程序中使用,并且可以污染命名空间。

替代方式:
const:const也可以用于定义常量表达式、函数或类成员函数等,具有更多的灵活性。
#define:#define只能用于定义简单的文本替换,不能定义表达式或函数。

const的用法
修饰局部变量

被修饰后的局部变量,一定需要初始化,并且变量的值不能被改变。

	//修饰局部变量:两种写法都一样,表示n的值不能被改变,需要初始化。
	const int n = 5;
	int const n = 5;
修饰常量字符串
//修饰字符串常量,使得程序员在不小心企图对字符串修改时立马报错,在编译器就检查了出来,方便高效
const char* str = "abcdefg";
修饰指针

被const修饰的指针分有常量指针和指针常量。

常量指针和指针常量的区别:常量指针是指针指向的值为常量,指针本身可以指向其他对象,而指针常量是指针本身为常量,指针的指向无法改变,只能指向一次初始化时的对象。

常量指针指的是指向的值为常量,不能通过指针本身去修改指向指向的值,但是可以通过变量本身或者其它指针去修改值。常量指针可以修改指针的指向。

	//常量指针:指针指向的内容是常量
	//常量指针说的是不能通过这个指针来改变变量的值,但是可以通过其它方法来改变这个变量的值
	//还有就是常量指针指向的值不能被改变,但是指针可以改变指向,也就是可以指向别的地址
	int a = 1;
	const int* p = &a;
	//*p = 6;err  不能通过这个指针来改变变量a的值
	a = 10;  //可以通过a本身来改变
	int b = 2;
	p = &b; //指针可以改变指向,也就是可以指向别的地址

指针常量值的是指针本身是常量,不可以修改指针的指向,但是可以修改指向所值的内容。

//指针常量:指针常量是指指针本身是个常量,不能在指向其他的地址。
	int c = 1;
	int* pc = &c;
	int* const ppc = &c;
	*pc = 2;//合法,可以通过指针常量修改所指向对象的值
	cout << c << endl;
	*ppc = 3; //合法,可以通过指针常量修改所指向对象的值
	cout << c << endl;

区分常量指针和指针常量,const读作常量,*号为指针,以*号为分界点。

const在*号的左边,即const * ,那么就是常量指针--指针指向的是常量,不可以通过指针修改内容,但是可以改变指针的指向。

const在*号右边,即* const,那么就是指针常量--指针是常量,不能修改指针的指向,但是可以修改内容。

指向常量的常指针---指针的指向不能被修改,也不能通过指针修改值,但是可以通过变量本身或其它指针去修改值。

    const int* const k = &c;
    c = 50;
    //*k = 8;err
修饰函数的参数 

防止指针指向的内容被修改--即传进来的参数的内容不可以被修改

跟修饰指针一样,如果在函数的参数列表中,const处于*号的左边,那么就是常量指针,表示该参数的内容不可以被修改。一般用于只进行访问操作,而不进行增删改造成的参数,可以有效地保护参数。

void func(char* str1, const char* str2);

str1的内容可以被修改,而str2的内容是不可以被修改的。 

防止指针的指向被修改--即传进来的指针参数不可以改变指向

void swap ( int * const a , int * const b )
修饰函数返回值

如果让以“指针传递”方式作返回值的函数中,返回值加上const修饰,那么函数的返回值的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。

//函数
const int* func();

//调用函数并返回
const int* ret = func();
int* pret = func();//由于返回值具有常性,而pret是变量,会导致返回值权限放大问题。

⑦explicit 

关键字explicit用于修饰类的构造函数,用来声明构造函数不可被用于隐式类型转换。通常情况下,当一个构造函数只有一个参数时,它可以被用于进行隐式类型转换。例如,如果一个类有一个接受int类型参数的构造函数,那么可以通过将一个int类型的值赋给该类的对象来进行隐式类型转换。

class MyNumber {
public:
    MyNumber(int value) : num(value) {}
private:
    int num;
};

int main() {
    MyNumber num = 10;  // 隐式类型转换
}

当构造函数被explicit修饰后,它将不再能够被用于隐式类型转换,而只能被用于显式类型转换。如果尝试使用隐式类型转换,编译器将会报错:

class MyNumber {
public:
    explicit MyNumber(int value) : num(value) {}
private:
    int num;
};

int main() {
    MyNumber num = 10;  // 编译错误,禁止隐式类型转换
    MyNumber num2(10);  // 显式类型转换
}

⑧volatile

volatile是一种类型修饰符,作用是当一个变量被声明为volatile时,编译器会禁止将该变量的读取和写入操作放入寄存器中,确保每次读取和写入都直接访问内存。使用场景一般是在多线程的共享变量上:

由于在多线程下,某个共享变量可能会在本线程不知情下被修改了值,而本程序是不会判断这个值有没有被修改过的,因此如果不用volatile,在多个线程中,很难确保当某个线程去寄存器读取数据时,该数据是被修改了的。而使用了volatile,那么所有线程都会直接去内存中读取,加上原子性等操作,就能保证获取到的数据是新修改的,以适应随时变化的未知情况,防止编译器或CPU对变量进行优化或重排。

const和volatile结合使用

一个变量被const和volatile同时修饰时,const可以确保该变量在本程序或本函数的主体内,它的值不会被修改。而volatile可以确保该变量不会被其它程序无意间或者恶意去修改。两者同时使用可以确保该变量兼具只读和防止优化。

一个指针被volatile修饰

指针可以被声明为volatile。volatile关键字的作用是告诉编译器该指针所指向的对象可能会在不同的时间被外部因素更改,并且在访问该指针时应该直接读取或写入内存,而不做任何优化。

一个常见的例子是,当我们在多线程环境下使用全局变量或共享内存时,可以使用volatile指针来确保在访问共享数据时的可见性和顺序性:

#include<iostream>
#include<thread>
using namespace std;

volatile int* ptr = nullptr;

void ThreadA()
{
    *ptr = 10;
}

void ThreadB()
{
    int value = *ptr;
    std::cout << value << std::endl;
}

int main()
{
    int data = 5;
    ptr = &data;

    // 启动两个线程
    std::thread threadA(ThreadA);
    std::thread threadB(ThreadB);
    

    threadA.join();
    threadB.join();

    return 0;
}

在这个例子中,声明了一个指向整数的volatile指针ptr。在主函数中,将变量data的地址赋值给ptr。然后启动了两个线程:ThreadA和ThreadB。

线程ThreadA将通过ptr写入值10,而ThreadB将通过ptr读取值并打印输出。由于ptr被声明为volatile,那么编译器计划意识到,*ptr的值可能被修改了,不能去寄存器中读取数据,而是直接从内存中读取,而内存中的数据被修改了10。这样,ThreadB可以正确读取到ThreadA写入的值,并打印输出,最终结果为10。如果不使用volatile修饰ptr指针,编译器可能会对指针进行优化,将*ptr读取操作缓存在寄存器中,而不是直接从内存中读取。因此,线程B可能会在缓存中读取到旧的值而非线程A写入的新值。

2.命名空间

在C/C++中,访问变量,都是默认查找规则。先在局部找,再全局找。

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

说白了,就是同一个项目组里面,如果负责项目A的人和负责项目B的人,都想要某个变量名称,但在代码合并后,那肯定会有变量名相同导致的bug,因此,在C++中,给这些全局变量,围上了一道墙--namespace,需要访问这些变量的时候,就得通过这道墙的大门了。

2.1 命名空间的定义

①定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

8e227875257349849a66ff34b248d7a4.png

#include<iostream>

namespace test
{
	int a = 10;
}
int main()
{
	int a = 20;
	std::cout << a <<std::endl;
	std::cout << test::a << std::endl;

	return 0;
}

 ②命名空间可以嵌套命名空间。

d61552731bde45b79aec66d376da397e.png

#include<iostream>
namespace test1
{
	int a;
	int b;
	int add(int left, int right)
	{
		return left + right;
	}

	namespace test2
	{
		int c;
		int d;
		int sub(int left, int right)
		{
			return left - right;
		}
	}
}
int main()
{
	test1::a = 10;
	test1::b = 20;
	int sum = test1::add(2, 5);
	test1::test2::c = 15;
	test1::test2::d = 25;
	int Sub = test1::test2::sub(10, 3);
	std::cout << test1::a << std::endl;
	std::cout << test1::b << std::endl;
	std::cout << sum << std::endl;
	std::cout << test1::test2::c << std::endl;
	std::cout << test1::test2::d << std::endl;
	std::cout << Sub << std::endl;

	return 0;
}

③同一个工程项目里面可以有多个相同名称的命名空间,编译器会把它们合在一块。
1e61667f2b0442f89b2a5020408fafb9.png

namespace test1
{
	int a;
	int b;
	int add(int left, int right)
	{
		return left + right;
	}

	namespace test2
	{
		int c;
		int d;
		int sub(int left, int right)
		{
			return left - right;
		}
	}
}
namespace test1
{
	int f;
	int k;
}

一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限在这个命名空间中。

2.2 命名空间的使用

那么,该如何使用定义的空间成员呢?

其实上面的代码已经说明了一部分了。

使用方法有三种:

①加上命名空间的名称和作用域限定符    如上面的:  test1 :: a;   test1就是命名空间的名称,::就是作用域限定符。

②使用using,将命名空间里面的某个成员引入。也就是说,把某个成员赶出这道墙,这样,就相当于普通的全局变量,谁都可以直接使用。

using test1::b;
int main()
{	
	std::cout << b << std::endl;
	std::cout << test1::b << std::endl;
	return 0;
}

③使用using namespace将命名空间名称引入。其实就是,将test1的围墙给拆了,里面的内容相当于普通的全局变量,谁都可以用。

using namespace test1;
int main()
{
	
	std::cout << b << std::endl;
	std::cout << test1::b << std::endl;

	return 0;
}

3.C++输入&输出

#include<iostream>
//std是C++标准库的命名空间名,C++将标准库的定义实现都放到了这个命名空间中
using namespace std;

int main()
{
	cout << "hello world" << endl;
	return 0;
}

使用说明:

1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
iostream >头文件中。
3. <<是流插入运算符,>>是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。
5. 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识。这会在往后的学习,可以更加的深入去学习和理解。

温馨提示:

1. 在日常练习中,可以直接using namespace std,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对
象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模
大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +
using std::cout展开常用的库对象/类型等方式。因为,这跟前面提到了,使用了using namespace std展开后,里面的内容,就想到全部变成了普通的全局变量之类的东西,很容易出bug。因为std这道墙被拆了!

4.缺省参数

4.1 缺省参数的概念

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

890b41a62d8f431d992a5744f8d9fa80.png

#include<iostream>
void fun(int a = 10)
{
	std::cout << a << std::endl;
}
int main()
{
	fun();
	fun(2);
	return 0;
}

 4.2 缺省参数的分类

①全缺省参数

45e46917fab1448e8e25fa63ea84428b.png

#include<iostream>
void fun(int a = 1,int b = 2,int c = 3)
{
	std::cout << a << " ";
	std::cout << b << " ";
	std::cout << c << std::endl;
}
int main()
{
	fun();
	fun(10);
	fun(10, 20);
	fun(10, 20, 30);
	return 0;
}

②半缺省参数

void fun(int a ,int b = 2,int c = 3)
{
	std::cout << a << " ";
	std::cout << b << " ";
	std::cout << c << std::endl;
}

说明:

半缺省参数必须从右往左依次给出,中间不能隔着给。                                                                    缺省参数不能同时在函数的声明和定义中出现。因为如果恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值,因此,如果一个函数有声明和定义,一般把缺省参数给在声明上。定义的时候,只需写出数据类型和变量名。

缺省值必须是常量或者全局变量。

5.函数重载

5.1 函数重载的概念

自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重
载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个
是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!

所谓重载,就是有多种意思。

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

51125309d50247fab8d9a29956b9f19e.png

#include<iostream>

int add(int x, int y)
{
	return x + y;
}
double add(double x, double y)
{
	return x + y;
}

int main()
{
	std::cout << add(1, 2) << std::endl;
	std::cout << add(1.1, 2.2) << std::endl;
	return 0;
}
#include<iostream>

//1.参数类型不同的函数重载
int add(int x, int y)
{
	return x + y;
}
double add(double x, double y)
{
	return x + y;
}
//2.参数个数不同的函数重载
void f()
{
	std::cout << "f()" << std::endl;
}
void f(int a)
{
	std::cout << "f(a)" << std::endl;
}
//3.顺序不同是指参数的类型的顺序不同
void s(int a, char b)
{
	std::cout << "s(int a,int b)" << std::endl;
}
void s(char b, int a)
{
	std::cout << "s(char b,int a)" << std::endl;
}
//不能这样
void f(int a, int b)
{
	
}
void f(int b, int a)
{

}

5.2C++支持函数重载的原理--名字修饰

这里只是简单说说,有个相对的了解。往后会在深入学习C++时,会深入学习这方面的。

在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参
数类型信息添加到修改后的名字中

7f6d05f570854a18954e2bb9b7c975ce.png
 

 对于函数重载和缺省参数的结合:

#include<iostream>

void f()
{
	std::cout << "f()" << std::endl;
}
void f(int a = 0, char b = 1)
{
	std::cout << "f(int a,char b)" << std::endl;
}
int main()
{
	f(10);
	f(10, 20);
	f();//歧义,二义性!!

	return 0;
}

上面代码中,是成立函数重载和缺省参数的,但是呢,因为一个有参一个无参,在调用是,会产生二义性。

int f(int a, int b)
{
	std::cout << "f(int a,int b)" << std::endl;
	return 0;
}
char f(int b, int a)
{
	std::cout << "f(int b,int a)" << std::endl;
	return 'a';
}

int main()
{
	f(1,1);
	f(2,2);

	return 0;
}

上面这种情况,因为无法确定返回值,而且参数的数据类型是相同的,所以无法构成函数重载!

6.引用

6.1 引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

ec96507cb13947c3b0621165c4b3c5f0.png

 形式:类型& 引用变量名(对象名) = 引用实体;

7595a12eaba443ce8eee82dc6ca0b5f6.png

int main()
{
	int a = 10;
	int& ra = a;//引用
	int& x = a;
	int& y = x;
	//它们都是a的别名。
	x++;//a = 11;
	y++;//a = 12;
	a++;//a = 13;
	std::cout << a << std::endl;

	return 0;
}

 注意:引用类型必须和引用实体是同种类型的

再来看个例子:

void swap(int& m, int& n)
{
	int temp = m;
	m = n;
	n = temp;
}

int main()
{
	int cc = 1, dd = 2;
	swap(cc, dd);//不用传地址了
	return 0;
}

6.2 引用特性

1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
 

void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}

66d2cf96174f4fff9a1d9d46fa907a07.png

6.3 常引用

一般来说,引用都会加上const,为什么呢?那什么时候加const,什么时候不加呢?

我们先来看看引用的场景。

6.4 引用的场景

我先来补充一点:权限的放大、缩小和平移。

什么是权限?就是某个变量、数据可读可写,或只可读或只可写。

权限的平移:a的权限是可读可写,然后,ra没有加上const,也跟a的权限一样,可读可写。

int a = 10;
//权限的平移
int& ra = a;

权限的放大:这个做法是不允许,因为,原本,对a,我只能读,但是我引用后,却想要可读可写。就好比如,我有一台手机,我用的时候小心翼翼的,爱护着使用。但是我借给某个人后,那个人又是拍又是摔的,能允许吗?

const int a = 10;
int& ra = a;

权限的缩小:原本,对a是可读可写,引用后,只可读。这是允许的。我的手机,我自己本来就又摔又拍,然后给某人用,他小心翼翼地爱护着,怕弄坏了要赔给我。这是允许的。

int a = 10;
const int& ra = a;

了解了const和不加const的权限后,我们接下来看看:

①做参数:

void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}

在做参数的时候,如果需要修改参数的值,则不加const。那么,我们传进去的参数,也不能加上const,因为会使权限放大。

其实做参数有几种情况:

        ※做参数时,直接引用,不加const,那么,只有a能传进去,b和ra不能,因为它们都加const修饰了,如果传进去,那就是权限放大了,我不能让你摔我的宝贝手机!

void Func(int& x)
{

}

int main()
{
	int a = 0;
    const int& b = 0;
    const int& rra = a;

	Func(a);
	Func(b);
	Func(rra);
	return 0;
}

        ※做参数时,加const修饰,那么不论是a还是b,rra,都能传入。

void Func(const int& x)
{

}

int main()
{
	int a = 0;
    const int& b = 0;
    const int& rra = a;

	Func(a);
	Func(b);
	Func(rra);
	return 0;
}

引用做参数的好处:减少拷贝,提高效率。在输出型的参数时,形参修改了,实参也修改了。

②做返回值:

这里再补充两个点:第一个点就是,函数在返回值时,函数栈帧销毁后,会创建一个临时变量,用来接收这个返回值,然后再传给调用函数的那个变量。而这个临时变量,具有常性!其实不止是函数返回值会创建临时变量,在数据类型转换的时候,也是这样转换的。

c4803b0469c44a8e9614d665c288d2a5.pngce4a971e52004dc29d8d09eb24d8bb82.png

 根据这个原理,我们来分析一下下面这段代码:

int Count()
{
	int n = 0;
	n++;
	// ...
	return n;
}

int main()
{
	const int& b = 10;

	double d = 12.34;

	cout << (int)d << endl;

	int i = (int)d; // 可以

	//int& ri = d; // 不可以

	const int& ri = d; // 可以
	cout << ri << endl;

	//int& ret = Count();
	const int& ret = Count();

	return 0;
}

第一个:b的数据类型是int&,如果直接给10,不给const的话,就会报错。为啥?因为10是一个常量,而b的类型是引用,代表着是10这个的别名,因此,b也得是个常量,所以需要加上const。

第二个:double类型的d,转换成int,不是将d的数据类型转换成int,而是在执行(int)d的时候,创建了临时变量,这个临时变量的类型是int,然后再传回给接收这个值的变量或者输出。而不需要const的原因,是变量本来就能接收常量,比如:int a = 10,但是不能int& a = 10,因为int是创建一个变量,int&是引用,需要看看引用的是常量还是变量,如果是int& b = a,那么就不需要加const,因为a是变量,而int&接收了变量,b是a的别名,不需要常性。

第三个:int& ri = d是错误的,加上const才是对的,这个不用再重复说了,因为临时变量是常量。。。

第四个:函数int Count();int& ret = Count();是错误的,因为,这个函数返回n时,需要创建临时变量,是个常性,int&引用常量,得加const。

第二个点是空间销毁,意味着:空间虽然还在,但是使用权不在我们,我们存进去的数据不被保护,虽然还能访问,但是访问到的数据,是个不确定值!因此,什么时候需要返回int&,还是int?

基于上面两点,我们看下面的分析:

        ※做返回值时,没有使用引用:从上面的分析可知,为啥没加const不行,就是因为返回来的是具有常性的临时变量,int&引用的是常量,需要加const。

int Count1()
{
	int n = 0;
	cout << &n << endl;

	n++;
	// ...
	return n;
}
int main()
{
	int ret1 = Count1();
    //int& reet1 = Count1();//不可以
	const int& rret1 = Count1();//可以

	return 0;
}

         ※做返回值时,使用了引用:使用了引用的返回值,没有创建临时变量 。如下的代码:因为返回的是一个n的引用,因为n的类型是int,那么返回的就是n的别名,也是int类型,那么,如果是int n = 10,那么可以有int& rret2 = n,权限的平移。所有,不需要加const!

     

int& Count2()
{
	static int n = 0;
	cout << &n << endl;

	n++;
	// ...
	return n;
}

int main()
{

	int ret2 = Count2();
	int& rret2 = Count2();

	return 0;
}

由于函数的栈帧销毁后,会将里面的内容也销毁,因此,在决定是否使用常引用来做返回值,就需要考虑以下问题:

出了函数作用域,返回变量不存在了,不能引用返回,因为引用返回的结果是未定义的。            出了函数作用域,返回变量还在,能够使用引用返回。

而使用引用返回的好处就是:减少拷贝,提高效率。还能修改返回值。

6.5传值、传引用效率比较

        传引用的效率比较高,不管是引用返回值还是引用参数

6.6引用和指针的区别

在语法上,引用是没有开辟新空间的,它跟引用的实体共用一个空间。而指针是需要开辟空间,来存放目标变量的指针

在底层,其实引用也是有开辟新空间的,因为引用是按照指针方式来实现的。

使用反汇编代码就能看出来:

0fbf2b25df6e4244b1a490b4a90eb3df.png

 最后,引用跟指针的区别:

引用和指针的不同点:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体

4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全

7.内联函数

7.1概念

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。如下图,如果加入了inline,那么在汇编中,函数的没有call指令来创建函数栈帧,在编译期间编译器会用函数体替换函数的调用。

cbb01047b7af41e79838554b6c99ab43.png
9ec745f8e5f240fc9aae8f79fc539beb.png
 

7.2特性

1. inline是一种以空间换时间(这里的空间是指编译出来的可执行程序的大小)的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率.

2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。

C++prime》第五版关于inline的建议:

a637335030fc40a091a768b794670b8f.png

 3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。

注意:只要函数是内联函数,无论编译器采取不采取inline的修饰,链接的时候在符号表里面都不会有这个函数的地址,也就导致无法找到这个函数,导致声明和定义的使用错误。

问:

宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
1. 常量定义 换用const enum
2. 短小函数定义 换用内联函数
 

8.auto关键字

8.1 类型别名思考

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在::

1. 类型难于拼写
2. 含义不明确导致容易出错

虽然使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:

typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}

解释:

在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。

8.2 auto简介

++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得

int TestAuto()
{
	return 10;
}
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = TestAuto();
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}

2850009f2ceb4ac3a9d1d47a1919f906.png
注意:

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

8.3 auto的使用细则

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

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

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

ddf881db2b0f43f29deb921cda7c51fb.png
2. 在同一行定义多个变量

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

void TestAuto()
{
    auto a = 1, b = 2;
    auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}

8.4 auto不能推导的场景

1. auto不能作为函数的参数

// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}

2. auto不能直接用来声明数组

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

9.基于范围的for循环

9.1 范围for的语法

在C++98中如果要遍历一个数组,可以按照以下方式进行
 

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
        array[i] *= 2;
    for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
        cout << *p << endl;
}

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

void TestFor()
{
    int array[] = { 1, 2, 3, 4, 5 };
    for(auto& e : array)
        e *= 2;
    for(auto e : array)
        cout << e << " ";
    return 0;
}

注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
 

9.2 范围for的使用条件

1. for循环迭代的范围必须是确定的

对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

注意:以下代码就有问题,因为for的范围不确定:因为传数组进去,传的就是首元素的地址,然而这是没有范围锁定的。

void TestFor(int array[])
{
    for(auto& e : array)
        cout<< e <<endl;
}

2. 迭代的对象要实现++和==的操作

以后会提到这点
 

10.指针空值--nullptr

10.1 C++98中的指针空值

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

可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(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(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。可以看到下面的结果,f(NULL)调用了第一个函数
8f35ed96189e41c1b58f962cbcd116c0.png

在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器
默认情况下将其看成是一个整形常量
如果要将其按照指针方式来使用,必须对其进行强转(void
*)0

 

因此,C++11引用了nullptr,解决了上面的问题。

注意:

1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。

2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。

3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

END~

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山雾隐藏的黄昏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值