C++入门(一)之基础知识点

目录

零、什么是c++

一、c++关键字

二、c++的命名空间

命名冲突

命名空间的定义

命名空间的使用

三、c++中的输入和输出

四、缺省参数

缺省参数的概念:

半缺省函数注意事项:

缺省函数的作用

五、函数重载(重要)

extern “C”

六、引用

引用实例

引用特性

常引用

实例1 

实例2 

实例3 

实例4

引用使用场景

1.0 做参数

2.0 做返回值

(1)值返回

(2)引用返回

值和引用作为返回值的性能比较

引用的作用主要体现在传参和传返回值

引用与指针的区别

引用和指针的不同点:

六、内联函数

内联函数特性

七、auto关键字(C++11)

auto的使用细则

auto不能推到的场景

八、 基于范围的for循环(C++11)

范围for使用条件

 九、指针空值nullptr(C++11)


零、什么是c++

     C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,
 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
     1982年,BjarneStroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。

此刻,以熟悉的hello world 开启c++的学习

#include<iostream>
using namespace std;
int  main()
{
    cout << "hello world" << endl;

    return 0;
}

一、c++关键字

在这里我们可以先了解一下

 二、c++的命名空间

命名冲突

或许很多人都知道在编写一个cpp程序时要加入 using  namespace  std ;但你知道这究竟是为什么嘛?
———在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。

namespace定义的是一个域,本质是解决C语言命名冲突的问题

eg:

针对以上程序我们的期望是,19行的scanf调用的是函数scanf,其他的scanf调用的是我们定义的变量,但是程序调用时遵循的是就近原则(从局部域开始找),19行的scanf调用不会去<stduio.h>中调用函数,而是调用了离它最近的scanf变量(16行),所以就产生了命名冲突的问题,C语言是解决不了这个问题的。

如果将他俩放到全局,就会出现重定义的问题 

eg:

通过namespace就可以完美解决问题

命名空间的定义

1. 定义命名空间首先用关键字namespace,在关键字后面加上名称,再加一对{}即可

namespace n1  //n1 为命名空间的名称
{
    int a = 0;  //命名空间中可以定义函数,也可定以变量
    int add(int a, int b)
    {
        return a + b;
    }
}

2. 命名空间可以嵌套

namespace n2
{
    namespace n3
    {
        namespace n4
        {
           //...
        }

    }
}

3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

namespace n1
{
    int Mul(int left, int right)
    {
        return left * right;
    }
}

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

命名空间的使用

C++库为了防止命名冲突,把自己库里面的的东西都定义在一个std的命名空间中去,要使用标准库中的东西,有三种方式

ps:#include<iostream.h> 老一点的C++标准用这个,VC6.0 可以用,没有std命名空间

1.0 指定命名空间 ,麻烦,每个地方要用都要指定,但它是最规范的方式

2.0 吧std整个展开,相当于库里面的东西都到全局域里面去了,看起来方便了,如果我们自己定义的东西跟库起冲突了,就没法解决。一夜回到解放前,所以规范的工程项目中是不推荐这种方式的,日常练习无所谓

3.0 对部分常用的库里面的东西展开

PS: ::  这是域作用限定符,C语言调用全局变量可以用

定义了如下的空间N1 ,使用它一共有三种使用方式

#include <iostream>
namespace N
{
    int a = 10;
    int b = 20;
    int add(int left, int right)
    {
        return left + right;
    }
    int sub(int left, int right)
    {
        return left - right;
    }
}

int main()
{
    //printf("%d", a);//直接使用编译器会报错
}

1.0 加命名空间名称及作用域限定符   什么时候用什么时候打开空间,安全,但是较为复杂

int main()
{
    printf("%d ", N::a);
    printf("%d ", N::b);
}

2.0  使用using namespace 命名空间名称引入,不安全但是简单

using namespace N;
int main()
{
    printf("%d ", a);
    printf("%d ", b);
    int c=add(1, 2);
    printf("%d ", c);
    int d=sub(4, 3);
    printf("%d ", d);
}

3.0 使用using将命名空间中成员引入  ,用哪个,提前打开哪个对象安全,推荐使用

using N::b;
int main()
{
    printf("%d ", N::a);
    printf("%d ", b);
}

想必到达这里就会明白为什么要用using namespace std;这一串话了(这段话告诉编译器我们要用名字空间std中的函数或者对象)
但是日后当我们在写成熟的代码的时候,一般不建议将标准命名空间全部打开,而是需要用库里的什么就打开什么。这就有效的防止了命名冲突

例如写一个hello world
第一种就是上面提到的
使用using namespace 命名空间名称引入,不安全但是简单

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

第二种,用哪个,提前打开那个对象
安全,推荐使用

#include <iostream>
using std::cout;
using std::endl;
int mian()
{
    cout << "hello world" << endl;
}

第三种,什么时候用什么时候打开std
安全,但是较为复杂

#include <iostream>
int main()
{
    std::cout << "hello world" << std::endl;
}

三、c++中的输入和输出

ostream 类型全局对象 cout

istream 类型全局对象 cin

endl 全局换行符号

又是老朋友:hello world

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

对比C语言的scanf 与 printf ,C++的cout与cin的区别是什么

cout与cin可以自动识别类型

eg: 

但cin,cout也不是在任何情况下都好用,

eg:

#include<iostream>
using namespace std;
struct Person
{
	char name[10];
	int age;
};

int main()
{
	int a = 0;
	cin >> a;  //scanf("%d ",&a);
	cout << a << endl;
	//这里cin就比scanf好很多

	struct Person p = { "小王",10 };
	printf("name:%s age:%d\n", p.name, p.age);//格式化输出printf更好一点
	cout << "name: " << p.name << "\n" << "age:" << p.age << endl;
    return 0;

}

 综合而言,两者可以混着用,谁好用谁。
2. 使用C++输入输出更方便,不需增加数据格式控制,比如:整形--%d,字符--%c

#include <iostream>
using namespace std;
int main()
{
    int a;
    double b;
    char c;
    cin >> a;  // >> 流提取操作符
    cin >> b >> c;
    cout << a << "  " << b << " " << c << endl;  // << 流插入操作符
    return 0;
}

四、缺省参数

缺省参数的概念:

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

说白了缺省函数就是,C++函数的参数中配的备胎

1.0全缺省,全部的参数都给了一个缺省值

#include<iostream>
using namespace std;
void fun(int a = 10, int b = 20, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl << endl;
}
int main()
{
	fun();          //不传参就用默认的
	fun(1, 2, 3);   //传了就用传了的
	fun(1);         //传了部分
	fun(1, 2);      //传了部分
}

执行结果:


 

 2.0 半缺省参数,部分的参数给了缺省值

#include<iostream>
using namespace std;
void fun2(int a, int b, int c = 30)
{
	cout << "a= " << a << endl;
	cout << "b= " << b << endl;
	cout << "c= " << c << endl << endl;
}
int main()
{
	fun2(1, 2);      // 没传就用默认值
	fun2(1, 2, 3);  // 传了就用传了的

}

执行结果: 

半缺省函数注意事项:

 1. 半缺省参数必须从右往左依次来给出,不能间隔着给
    正确的半缺省函数写法:

错误的半缺省函数写法:

原因也很简单,因为函数传参的时候是从左向右依次传参的

 2. 缺省参数不能在函数的声明和函数的定义中同时出现

注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

//a.h
void TestFunc(int a = 10);
// a.c
void TestFunc(int a = 20)
{}

   3.缺省值必须是常量或者全局变量
   4.C语言不支持(编译器不支持)

缺省函数的作用

   缺省函数的作用就是为了使函数调用更加灵活,乍一听感觉好像也没啥用,但在一些情况下发挥着巨大的作用;

eg:我在这写了个栈及栈的初始化,初始化我写的是开四个空间,在主函数中,假设我知道栈里面至少要存100个数据,这个初始化显然不合理只能开4个,接着就开始增容,增到100,同时增容是要付出代价的,有消耗的,所以就很不方便。但假设我知道这个栈里面最多存10个数据,而对这个栈来说,我们写的初始化就很合理,增容的消耗也小。所以就造成了不方便的局面。

struct Stack
{
	int* a;
	int size;
	int capacity;
};

void StackInit(struct Stack* ps)
{
	ps->a = (int*)malloc(sizeof(int) * 4);
	ps->size = 0;
	ps->capacity = 0;
}
int main()
{
	struct Stack st1;
	//假设我知道栈里面至少要存100个数据
	StackInit(&st1);
	//这个初始化显然不合理,接着就开始增容,增到100,同时增容是要付出代价的,有消耗的


	struct Stack st2;
	//假设我知道这个栈里面最多存10个数据
	StackInit(&st2);
	//而对这个栈来说,我们写的初始化就很合理,增容的消耗也小

	return 0;
}

这时候缺省参数的作用就体现出来了,我们将初始化的函数多传一个缺省值4

void StackInit(struct Stack* ps, int  InitCapacity = 4)
{
    ps->a = (int*)malloc(sizeof(int) * InitCapacity);
    ps->size = 0;
    ps->capacity = InitCapacity;
}

假设我知道栈里面至少要存100个数据,缺省值就传100。

假设我知道这个栈里面最多存10个数据缺省值就传10。

假设我不知道这个栈里面可能存多少数据,缺省值就啥也不传,非常方便的解决了问题,同时体现出缺省函数的作用:使函数调用更加灵活。

int main()
{
	struct Stack st1;
	//假设我知道栈里面至少要存100个数据
	StackInit(&st1,100);
	
	struct Stack st2;
	//假设我知道这个栈里面最多存10个数据
	StackInit(&st2,10);
	
	struct Stack st3;
	//假设我不知道这个栈里面可能存多少数据
	StackInit(&st2);

	return 0;
}

五、函数重载(重要)

     自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。( 函数重载必须在同一作用域 )

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

     C语言不允许定义同名函数,C++可以,要求就是函数的(参数个数 或 类型 或 顺序)必须不同。

 eg:下列函数都够成重载

#include<iostream>
using namespace std;
int Add(int left, int right)
{
	return left + right;
}
char Add(char left, char right)
{
	return left + right;
}
double Add(double left, double right)
{
	return left + right;
}
long Add(long left, long right)
{
	return left + right;
}
int main()
{
	cout << Add(10, 20) << endl;     //匹配int型
	cout << Add(10.0, 20.0) << endl; //匹配double型
	cout << Add(10L, 20L) << endl;   //匹配long型
	cout << Add('1', '2') << endl;   //匹配char型
	return 0;
}

结果如下:

这两个函数构成重载吗? 

构成重载,但是不能同时调用,因为编译器识别不了到底应该调用谁 

注意:这两个函数就不构成运算符重载,因为参数的类型,个数都一样,只是返回值不同

short Add(short left, short right) 
{
 return left+right;
}
int Add(short left, short right)
 {
 return left+right;
 }

下面两个函数构成重载吗?

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

答案:不构成,他俩类型相同,只是缺省参数不同`

1.0为什么C语言不支持重载,C++支持重载?C++底层是如何支持重载的

以这单代码为例

编译链接的过程

第一步 预处理---头文件展开+宏替换+去掉注释+条件编译--func.i test.i

第二步 编译--检查语法,生成汇编代码--func.s test.s

第三步 汇编--把汇编代码转成二进制机器代码--func.o test.o

第四步 链接--把.o文件链接到一起,合并符号表,生成可执行程序--a.out

C语言不支持函数重载,因为编译的时候,两个函数重载,函数名相同,在func.o符号表中存在歧义和冲突,其次在链接的时候也存在歧义和冲突,因为他们都是直接通过使用函数名去标识和查找。而重载函数,函数名相同。

C++的目标文件符号表中不是直接用函数名来标识和查找函数。它引入了函数名修饰规则(不同编译器下不同)。

g++的函数名修饰规则_Z+函数名长度+函数名+参数首字母

有了函数名修饰规则,只要参数不同,func.o符号表里面重载的函数就不存在二义性和冲突了。链接的时候,test.o的mian的函数里面去调用两个重载的函数,查找地址时也是明确的。

ps:main函数中调用函数时,如果当前文件有函数的定义,那么编译的时候就填上地址了;如果当前文件只有函数的声明,那么定义就在其他.cpp中,编译时没有地址,只能在链接的时候去其他.o符号表中根据函数修饰名字去找,这就是链接的重要工作。 

extern “C”

实际场景中可能出现两种情况

1.有个东西是C++写的,现在有个C语言的程序,需要调用这个东西

2.有个东西是C写的,现在有个C++的程序,需要调用这个东西

六、引用

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

int main()
{
	int a = 10;
	cout << "a:" << a << endl;

	//&在类型后面,就是引用的意思
	int& b = a; // b就是a的引用(别名)

	b = 20;
	cout << "a:" << a << endl;

	int& c = b;  //c就是b的引用

	
}

通过监视我们可以看出完全符合引用的概念。

引用实例

1.0 我们最熟悉的也就是交换两个值

void swap(int* a, int* b) //C语言中的交换需要指针(形参的改变不影响实参的改变)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void swap(int& a, int& b) //C++的交换,用引用即可
{
	int r = a;
	a = b;
	b = r;

}
int main()
{
	int a = 10;
	int b = 20;
	swap(&a, &b); //C语言传地址
	swap(a, b);   //C++传引用
}

引用特性

1. 引用在定义时必须初始化

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

int main()
{
    //一个变量可以有多个别名
    int a = 0;
    int& b = a;
    int& c = b;
    int& d = c;

}

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

对于指针:

 由监视可以看出,p的指向发生了改变

 对于引用

 由监视可知,开始引用了a,就只能引用a,不能去引用x。

常引用

首先明白const关键字 ,const修饰默一遍量就代表该变量不能修改了

实例1 

const修饰代表a不能修改
 int& ra = a;   错误原因: ra引用a属于权限的放大,a是可读的,ra却是可读可写的,所以不行
 const int& ra = a;正确原因:  加上const后ra也变成只能读了,和原先的a权限保持一致,所以正确

实例2 

const int& crb = b;//正确原因,crb引用b属于权限的缩小,b是可读可写的,crb是可读的,权限缩小,所以可行

实例3 

首先明白

 上述转换是可以的,发生了隐式类型转换,首先产生一个中间临时变量,这个中间临时变量是double类型的,将c赋给临时变量,再将临时变量赋给d

   double& rc = c; 很多人说这种写法错误的原因是:rc是double类型,c是int类型的,这种解释是错误的。 而我们发现加了个const后就可以编过了,原因:发生类型赋值过程当中,将int赋给double,中间就会产生临时变量,临时变量是double类型,所以rc引用的是这个临时变量,而这个临时变量具有常性(只是可读的),所以加了const后编译通过,类似于eg1。

实例4

对这种传值,传a与c都是没有问题的

如果 假设x是一个大对象或者说是深拷贝的对象,那么尽量用引用传参,减少拷贝,但这时候a就属于权限的放大,所以程序编译不过

 

 加入const就可以解决,如果f函数中不改变x,建议尽量用const引用传参

引用使用场景

1.0 做参数

C语言中交换需要传地址,因为形参是实参的拷贝

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

C++中直接传引用就可以实现交换,既没有传值也没有传地址,传的是引用 

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

同样,他俩也构成重载,但是调用swap的时候会存在歧义,编译器不知道该调用谁

传值、传引用效率比较
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
	TestRefAndValue();
}

ps:使用引用传参,如果函数中不改变参数的值,建议使用const Type & ,因为它可以接受各种类型的对象

eg:

void StackPrint(const struct Stack& st)
{}

2.0 做返回值

(1)值返回


     传值返回时(所有的传值返回都会生成一个拷贝),调用Add完成后会产生int类型的临时变量,c给了临时变量,临时变量再给给ret
返回的对象是c的拷贝

  通过下图我们可以证明确实产生了临时变量(ret引用的不是c,其实是这个临时变量,临时变量具有常性,所以第二个引用编过了)

为什么会产生临时变量呢?

如果直接把c给给ret会出现很多问题,函数Add执行完成以后,函数栈帧就销毁了,再去取c的值给给ret就会出现很多问题,有可能返回3,也有可能返回随机值,这取决于栈帧销毁的时候清不清理空间,严格意义上来说都是非法访问的,一块空间已经还给系统(销毁),我们就不能去访问了 ,所以说中间就会产生一个临时变量

这个临时变量存在哪呢? 

1.0 如果c比较小(4个字节或者8个字节),一般是寄存器充当临时变量。

2.0如果c比较大,临时变量放在调用Add函数的栈帧中去

(2)引用返回

 传引用返回 ,意思就是不会生成c的拷贝返回,直接返回c的引用

 主函数中并没有加const编译通过,所以证明返回的是c的引用

这段代码看着是没问题,编译器也通过了,但它实际上却存在着问题:

1.0 存在非法访问,因为Add(1,2)的返回值是c的引用,所以Add栈帧销毁以后,回去访问c位置的空间

2.0 如果Add函数栈帧销毁时,清理空间,那么取c值的时候取到的就是随机值,给ret就是随机值 ,这个取决于编译器的实现。ps:vs下销毁栈帧是没有清理空间数据的

 编译器有时候也会清理空间数据,例如对malloc函数

 所以现在的用法是一个错误的用法。

那么如果所有的编译器在栈帧销毁时都不清理空间,那是不是就可以这样用了?

答案也是不行的。

 这个时候ret就是c的引用(别名),就是c。为什么ret是3,最终会变成30呢?

第一次调用Add函数建立栈帧,调用完成后销毁,此时ret也就是c的值是3,然后再调用一次Add函数两次函数调用的栈帧大小是一样的,覆盖了同样位置的空间,c变成是30,函数栈帧销毁,再次打印ret,ret就是c,变成了30

PS:不同的栈帧不一定在同一块空间,原来的空间已经销毁了,在调用函数的时候就可以继续用之前的                                    

cout同样也会建立栈帧,每个栈帧的大小取决于这个函数里面会开辟多少,这个函数里面的局部变量和参数越多,栈帧就越大,cout同样也是一个函数,他为什么没有影响ret,就说明cout的栈帧开的很小,没有覆盖到c,如果覆盖到c,那么c就会改变

eg:

这就说明printf开的栈帧将c给覆盖掉了 

栈帧是调用完了就销毁掉,销毁掉以后,别人调用这可以利用这块栈帧,没对c覆盖就没影响,对c覆盖了就有影响

注意:一个函数调用就会向下空间建立一个栈帧,函数调用结束,栈帧就会销毁

引用返回的使用情况
实际过程中,出了函数作用域,返回对象就不存在了,不能用引用返回,但是加了static保证他出了作用域不会被销毁,就可以用引用返回了



总结下:如果函数返回时,出了函数作用域,如果返回对象还未还给系统(也就是没销毁,例如加了static),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。

值和引用作为返回值的性能比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是 传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是 当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回  ---每次拷贝40000字节
A TestFunc1() { return a; }
// 引用返回 ---没有拷贝
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
	TestReturnByRefOrValue();
}

调用一万次比较他们的性能,结果如下

由此发现,引用返回比值返回快很多 

引用的作用主要体现在传参和传返回值

1.0 引用传参和传返回值,在有些场景下,可以提高性能(当参数和返回值是比较大的变量时,传引用传参和传引用做返回值可以提高效率,深拷贝...)
2.0 引用传参和传返回值,输出型参数和输出型返回值。通俗来说,在有些场景下,形参的改变可以改变实参,有些场景下,引用返回,可以改变返回对象。

引用与指针的区别

int main()
{
    int a = 10;
    //在语法上,这里给a这块空间取了一块别名,没有开辟新空间
    int& ra = a;
    ra = 20;
    //在语法上,这里定义个指针变量,开辟了4个字节,存储a的地址
    int* pa = &a;
    *pa = 20;
}

从汇编来看,引用的底层也是类似指针存地址的方式进行的

引用和指针的不同点:

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

六、内联函数

调用函数需要建立栈帧,建立栈帧要保存一些寄存器,结束后又要恢复...,这些都是有消耗的,对这些频繁调用的函数如何进行优化呢?

c++频繁调用的函数,定义成inline,会在调用的地方展开,没有栈帧的开销

c语言为了小函数避免建立栈帧的消耗-》》提供了宏函数支持,预处理阶段展开

提到宏,我们进行一个小测试,请读者自己写一个Add的宏,先不要看答案。

普遍存在的错误写法
#define Add(int x,int y) return x+y;//典型的错误写法,是宏而不是函数
#define Add(x,y) x+y; //错误 分号问题


#define Add(x,y) x+y  // 错误 优先级问题 #define Add(x,y)  (x+y)  // 错误 +的优先级大于|和&

标准写法

记住宏原理是替换,你替换一下看看对不对
#define Add(x,y) ((x)+(y))

既然c语言已经解决了,为什么c++还要提供inline函数呢?(宏函数的缺点)
a.不支持调试 b.宏函数语法复杂容易出错 c.没有类型安全的检查

inline int add(int x,int y)  //直接在函数前加一个inline构成inline函数
{
    return x + y;
}
int main()
{
    int c = 0;

}

内联函数特性

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

4.类里面定义的函数默认就是inline函数。

这里解释以下什么叫以空间换时间

七、auto关键字(C++11)

C++11 中,标准委员会赋予了 auto 全新的含义即: auto 不再是一个存储类型指示符,而是作为一个新的类型 指示符来指示编译器, auto 声明的变量必须由编译器在编译时期推导而得
int a = 10;
auto b = a;//类型声明成auto,可以根据a的类型自动推导b的类型,a是int类型,b也是int类型

eg: 

int TestAuto()
{
	return 10;
}
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = TestAuto();

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

	//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
	return 0;
}


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

auto的使用细则

1. auto 与指针和引用结合起来使用, auto 声明指针类型时,用 auto auto* 没有任何区别,但用 auto 声明引用类型时则必须加 &
int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	int& y = x;
	auto c = y;
	auto& d = x; //指定了d是x的引用

	//typeid打印变量的类型
	cout << typeid(x).name() << endl;
	cout << typeid(a).name() << endl;
	cout << typeid(b).name() << endl;
	cout << typeid(y).name() << endl;
	cout << typeid(c).name() << endl;
}

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

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

auto不能推到的场景

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

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

2.auto不能用来推导数组

八、 基于范围的for循环(C++11)

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

    int arry[] = { 1,2,3,4,8,9 };
	for (int i = 0; i < sizeof(arry) / sizeof(int); i++)
	{
		cout << arry[i] << " ";
	}

为了简化操作,C++11引入了基于范围的for循环                                                                           for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围

for (auto e : arry)
	{
		cout << e << " ";
	}

 同样也可以自己定义,但不如auto好用


	for (int x : arry)
	{
		cout << x << " ";
	}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环

如果我们想用范围for来改变数组中的值(让数组中的每一个值都加1),我们该如何做呢?

首先我们可能会简单的认为只要将e++即可

int main()
{
	int arry[] = { 1,2,3,4,8,9 };

	for (auto e : arry)
	{
		e++;
	}

	for (auto e : arry)
	{
	 cout << e << " ";
	}
	
}



但通过结果发现,数组并没有被改变,原因就是:这个范围for 是自动遍历,依次取出arry中的元素,赋值给e,直至结束,e的确实改变了,但打印时,e又被arry中的元素依次赋值,所以数组并没有被改变。

那么该如何实现呢? 我们可以用引用实现,e是arry数组中每个元素的别名,e的改变就会影响arry数组中值的改变。

int main()
{
	int arry[] = { 1,2,3,4,8,9 };

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

	for (auto e : arry)
	{
	 cout << e << " ";
	}
	
	return 0;
}

 那么用指针可以吗? 答案是不行的,因为arry中数据是int,不能将int 转化为int *

范围for使用条件

范围for必须是数组名,传参的时候相当于传了数组的首地址,也就是传了指针,不符合范围for的语法规定

  

 九、指针空值nullptr(C++11)

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

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到, NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量 。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如
void f(int a)
{
	cout << "f(int)" << endl;
}


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


int main()
{
	f(0);
	f(NULL);
	return 0;
}

 

程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖,所以引入nullptr来解决这一问题。

 注意
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。 

  • 12
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 《程序设计C入门一百题》是一本面向初学者的编程教材,旨在帮助读者快速入门C语言编程。该书包含了一百个编程练习题,涵盖了C语言的基本语法、数据类型、循环结构、条件语句和函数等方面的内容。 这本书的设计思路非常贴合初学者的学习需求。首先,书中的编程题目从易到难,循序渐进,每个题目都有详细的题目描述和要求,帮助读者理解题目的意思。 其次,书中提供了每个练习题的参考答案,读者可以通过对比参考答案来检查自己的程序是否正确。这不仅可以加强读者的练习效果,还可以培养读者对编程思路和逻辑的理解。 此外,书中还对一些常见的编程问题进行了详细的讲解,如变量的定义和使用、循环结构的运用、函数的调用等。这些讲解通过简单明了的语言,配合示例代码和图表,能够帮助读者更好地理解和掌握这些知识点。 总之,《程序设计C入门一百题》可以作为初学者入门学习C语言编程的良好教材。通过完成其中的练习题,读者可以逐步提高自己的编程能力和解决问题的能力。而且,该书内容简洁明了,适合自学和课堂教学使用。无论是对于学校的计算机专业学生还是对于自学编程的人来说,都是一本值得推荐的书籍。 ### 回答2: 程序设计C入门一百题是一套经典的C语言练习题集,旨在帮助初学者巩固和提升编程能力。以下是对该题集的回答: 程序设计C入门一百题的设计非常精妙,从基础到进阶,涵盖了C语言编程的各个方面。这套题集适合C语言初学者,通过完成这些题目,可以帮助初学者系统地掌握C语言的基本语法和常用库函数的应用。 这套题集的每个题目都有其独特的特点和难度,需要我们运用C语言知识进行分析和解决。比如,在题目中给出一段代码,要求我们预测输出结果,这需要我们熟悉C语言的运算符、数据类型和优先级规则。还有一些题目要求我们编写函数或程序,实现特定的功能,这需要我们对C语言的语法和常用库函数有一定的掌握。 通过解答这些题目,我们可以学习到如何使用C语言进行输入输出、条件判断、循环控制、数组和字符串等基本操作。同时,这些题目也锻炼了我们的逻辑思维和编程能力,提高了我们解决问题的能力。 虽然程序设计C入门一百题对初学者来说可能有一定的难度,但是通过学习和解答这些题目,我们将逐渐形成自己的编程思维,提高自己的代码实现能力。同时,题目中的提示和解析也能够提供对疑惑点的解答和深入理解。 总之,程序设计C入门一百题是一套优秀的C语言练习题集,通过学习和解答这些题目,初学者可以系统地学习和掌握C语言的基本知识和编程技巧,为日后的进一步学习和应用打下良好的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值