C++入门基础

C++入门基础

C++的第一个程序

C++兼容C语言,所以在C++中用C语言写也是可以运行的,C++是在C的基础上增加了类和对象、封装、继承、多态等特性。

#include <iostream>
using namespace std;

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

我们接下来就一一来介绍这个程序中的符号含义。

命名空间

namespace的价值

namespace是一个关键字,我们先来看看为什么需要有namespace

#include <stdio.h>
//#include <stdlib.h>
int rand = 10;
int main()
{
	printf("%d", rand);

	return 0;
}

正常情况下,这个程序是没毛病的,但是如果我加上stdlib.h这个头文件,就会报错

#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
	printf("%d", rand);

	return 0;
}

在这里插入图片描述

这就是C语言的第一个问题----命名冲突,在同一个域中,不能定义同样的,因为会发生冲突,这里rand是C中的一个函数。万一公司里面你和你同事用了一个命名,一合并就玩完了,所以为了解决这个问题,就有了namespace

namespace的定义

• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中

即为命名空间的成员。命名空间中可以定义变量/函数/类型等。

• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下

⾯的rand不在冲突了。

• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/

类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响

编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期

#include <stdio.h>
#include <stdlib.h>

//域
namespace cbn
{
	int rand = 10;
}
int main()
{
	printf("%p", rand);//这里的rand访问的就是全局的,注意这里的rand是函数,我们就打印地址就行,否则会报错
	printf("%d", cbn::rand);//加上域访问限定符就可以访问域中的了

	return 0;
}

再看下面一段程序:

int a = 10;

int main()
{
	int a = 1;
	printf("%d", a);//这里访问的是局部的a
}

根据就近原则,这里默认访问的是局部的a,因为编译的时候会查找名字的出处,会先在局部找,再在全局找。那我现在就想访问全局的a咋办?

使用域作用限定符::

printf("%d", ::a);

::左边啥都不写,就默认在全局中找

在这里插入图片描述

C和C++都有一个原则,使用一个函数都需要找到它的声明和定义,隔离在不同的域,找的时候就不会冲突了。

namespace cbn
{
	int rand = 10;
}

比如这里的rand,rand还是定义在全局的,只是会受cbn这个域限制而已,注意,不能把cbn这个域定义在局部。

namespace cbn
{
	// 命名空间中可以定义变量/函数/类型
	int rand = 10;
	int Add(int left, int right)
	{
		return left + right;
	}
	struct Node
	{
		struct Node* next;
		int val;
	};
}

int main()
{
	//如何使用域中的自定义类型:
	struct cbn::Node p1;//struct是关键字,我们这里限定的是名字,所以要把访问限定加在结构体的名字前面
	return 0;
}

namespace的嵌套定义

• namespace只能定义在全局,当然他还可以嵌套定义。

• 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。

//嵌套定义:
namespace cbn
{
	namespace cbn1
	{
		int rand = 1;
		int Add(int left, int right)
		{
			return left + right;
		}
	}

	namespace cbn2
	{
		int rand = 2;
		int Add(int left, int right)
		{
			return (left + right) * 10;
		}
	}
}

int main()
{
	printf("%d\n", cbn::cbn1::rand);
	printf("%d\n", cbn::cbn2::rand);
	printf("%d\n", cbn::cbn1::Add(1, 2));
	printf("%d\n", cbn::cbn2::Add(1, 2));

	return 0;
}

在cbn1和cbn2中还可以继续嵌套

多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样

这个我们在后续会经常使用,以后会使用的。

• C++标准库都放在⼀个叫std(standard)的命名空间中。

std就是祖师爷封装的标准库

命名空间的使用

• 指定命名空间访问,项⽬中推荐这种⽅式。

cbn::cbn1::rand

• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。

namespace cbn
{
	int a = 10;
	int b = 10;
}

using cbn::a;//把a展开

• 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常⼩练习程序为了⽅便推荐使⽤。

namespace cbn
{
	int a = 10;
}

using namespace cbn;

int main()
{
	printf("%d", a);//这里就可以直接使用a了

	return 0;
}

注意,展开头文件是把头文件内容拷贝过来,编译代码的时候是没有头文件的,因为.h会在用的地方展开,展开命名空间不是拷贝,相当于把命名空间那个域给拆开了。

输入和输出

  • 包含头文件iostream

input output stream,是标准输入、输出流的库,只有特别特别老的编译器才会有iostream.h(类似于vc.60)

  • std::cin 是istream类的对象,主要面向窄字符的标准输入流(后续会介绍wchar 宽字符)

  • std::cout 是ostream类的对象,主要面向窄字符的标准输出流

像整型、浮点型这些都是内存里面的存储,方便运算,而我们常见的是在文件中,在文件和网络中,就变成字符了

int i = 10;
cout << i;

比如这里,我们cout i,虽然i是int类型的,但是不管是cout 还是printf,这个整型都是转换成了字符再输出出去的。输出到的位置就是我们的控制台(console)。cout和cin输出输入的位置自动是转换到控制台位置,我们如果想输出到文件、数据库也可以,这个后续再介绍

补:控制台在linux平台下叫终端

  • <<是流插⼊运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)

  • 使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出

int a = 10, b = 9;
cout << " " << a << " " << b;

cin同理,也是自动识别类型

int a, b, c;
double d;
cin >> a >> b >> d;//可以多个
cin >> c;//可以单个
  • std::endl 是⼀个函数,流插入输出时,相当于插入⼀个换行字符加刷新缓冲区。

在这里插入图片描述

end line的底层不是\n,而是一个函数,它的底层是运算符重载,重载了一个函数指针,但是我们可以简单认为,它的行为就等价于put(‘\n’)

在这里插入图片描述

  • 这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含< iostream >间接包含了。vs系列编译器是这样的,其他编译器可能会报错

C++会间接包C的文件

  • 控制精度

建议直接用C的,因为C++的用着不爽。

  • 补充:因为C++要兼容C语言,像输入输出它是先会放在缓冲区,到一定程度再刷新出去,所以是需要付出一定代价的,在io需求比较高的地方,如部分大量输入输出的竞赛题或者比赛中,用cout/cin可能oj过不了,效率没有scanf和pritnf高,这时候我们可以考虑C和C++换着用,或者加上这三行代码提高cin/cout的效率

ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

缺省参数

  • 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)

void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
	Func(); // 没有传参时,使用参数的默认值
	Func(10); // 传参时,使用指定的实参
	return 0;
}
  • 缺省分为全缺省和半缺省(部分缺省),但是半缺省有规定,必须从右向左连续缺省,没有为什么,是祖师爷设计的

//全缺省
void Func1(int a = 10, int b = 10, int c = 29)
{

}
//半缺省
void Func2(int a, int b = 10, int c = 10)
{

}
//这样写不行,必须从右向左连续缺省,不能跳跃,不能从左向右
void Func3(int a = 10, int b, int c = 10)
{

}
void Func4(int a = 10, int b = 10, int c)
{

}
  • 我们调用带缺省参数的函数时,C++规定必须从左到右依次给实参,不能跳跃给实参,不能间隔给实参

    //全缺省
    void Func1(int a = 10, int b = 10, int c = 29)
    {
    
    }
    //半缺省
    void Func2(int a, int b = 10, int c = 10)
    {
    
    }
    
    int main()
    {
    	Func1();
    	Func1(1);
    	Func1(1,2);
    	Func1(1,2,3);
    	
    	Func1( ,2, );//不能这样传!!!
        
        Func2();//不能这样,因为是半缺省,至少传一个
        
    }
    

缺省参数在实践中的意义

比如我们的栈

void STInit(ST* ps, int n = 4)//n表示开空间大小
{
    assert(ps && n > 0);
    ps->a = (STDataType*)malloc(n * sizeof(STDataType));
    ps->top = 0;
    ps->capacity = n;
}

这里我们给n个缺省值,就是4,默认开4个空间,假如我们想要1000个空间,那就传第二个参数1000,在初始化就开好空间,避免扩容

int main()
{
    ST s1;
    STInit(&s1);
    // 确定知道要插⼊1000个数据,初始化时直接开好,避免扩容
    ST s2;
    STInit(&s2, 1000);
    return 0;
}
  • 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值,否则会报重定义的错

比如:

//Stack.h文件
void STInit(ST* ps, int n = 4);
//Stack.cpp文件
void STInit(ST* ps, int n = 4)//这里重复给n的缺省值就会报错,规定只能在声明中给缺省值
{
    assert(ps && n > 0);
    ps->a = (STDataType*)malloc(n * sizeof(STDataType));
    ps->top = 0;
    ps->capacity = n;
}

这里主要是考虑到万一你声明和定义给的缺省值不一样,编译器就不知道选哪个了

函数重载

C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了静态多态行为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的

  • 参数类型不同
int Add(int a, int b)
{
	return a + b;
}

double Add(double a, double b)
{
	return a + b;
}

int main()
{
	Add(1, 2);
	Add(1.1, 1.3);

	return 0;
}
  • 参数个数不同
void f()
{
}

void f(int a)
{
}
  • 参数顺序不同(本质也是类型不同)
void f(int a, char c)
{}

void f(char c, int a)
{}

返回值不同不能作为重载条件,因为编译器无法识别

函数重载的一个大坑

void f()
{

}

void f(int a = 10)
{

}

这俩该调哪一个?这俩不调就没问题,一调就出问题,他们虽然构成函数重载,但是无参调用时会有调用歧义,这解决不了,实在要这样写,把他俩放在不同的域就行。但是这时候他俩就不是函数重载了,因为作用域不同

namespace ccc
{
	void f()
    {

	}
}

void f(int a = 10)
{

}

引用

概念

给已有的空间起别名,就是小名,类似于林冲,外号“豹子头”,取别名不用开空间

类型& 引用别名 = 引用对象

int main()
{
    int a = 0;
    // 引⽤:b和c是a的别名
    int& b = a;
    int& c = a;
    // 也可以给别名b取别名,d相当于还是a的别名
    int& d = b;
    ++d;
    // 这⾥取地址我们看到是⼀样的
    cout << &a << endl;
    cout << &b << endl;
    cout << &c << endl;
    cout << &d << endl;
    return 0;
}

在这里插入图片描述

引用的特性

  • 引用必须在定义的时候初始化
int& ra;//不能这样写,必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,就不能再引用别人,即不能改变引用的指向
int a = 10;
int& b = a;
int& c = a;
int d = 20;
b = d;//这里表示的是赋值,因为引用不能改变指向

C++的引用和Java这类的语言是很不同的,Java的引用可以改变指向,也不是特别需要初始化的,Java的引用在特性上更像C++的指针和引用的结合,C++的引用是为了和指针相辅相成的。

引用的使用

  • 引用传参和指针传参功能类似,在一些场景下可以替代指针,简化代码

比如以前C语言的Swap函数

void swap(int* px, int *py);

现在用引用就可以

void swap(int& px, int& py);

当然,指针的方法还是可以继续用的。

int main()
{
	int a = 10,b = 5;
	swap(a,b);//这里px就是a的别名,py就是b的别名
}

现在我们就可以把这个用到栈的实现中,替代一级指针和二级指针,比如

// int STTop(ST& rs)
int& STTop(ST& rs)
{
    assert(rs.top > 0);
    return rs.a[rs.top];
}

我们要改变栈,现在传引用过去就行

int main()
{
	ST st;
	STTop(st) += 10;
	
	return 0;
}

注意,引用没有空引用,也就是不能nullptr&

我们再来上点强度

typedef struct ListNode
{
    int val;
    struct ListNode* next;
}LTNode, *PNode;
    
void ListPushBack(PNode& phead, int x)
{
    PNode newnode = (PNode)malloc(sizeof(LTNode));
    newnode->val = x;
    newnode->next = NULL;
    if (phead == NULL)
    {
        phead = newnode;
    }
    else
    {
        //...
    }
}

这里的PNode& 是啥?struct ListNode*&

就是struct ListNode* 的别名,以前这里我们需要用二级指针来接收,这里只需要用一级指针的别名即可,也就是这里的PNode

  • 引用在实践中主要是用引用传参和引用做返回值,从而减少拷贝,提高效率和改变引用对象的同时改变被引用对象

我们以前的传值传参是一个拷贝

void func(int x)//形参x是a的拷贝,它的改变不影响a
{

}

int main()
{
	int a = 10;
	func(a);//这里就是把a拷贝给形参x
	return 0;
}

如果以后a比较大,再使用传值传参,拷贝的消耗就会很大,C语言采用的措施是用指针,我们这里就可以使用引用来解决

void func(int& x)
{
	++x;
}

传引用返回会在类和对象再具体说明。

传值返回也会生成一份拷贝。

在这里插入图片描述

这个时候,使用传引用返回就会很爽

但是,不是所有情况都要用传引用返回,比如:

int& func()
{
	int a = 10;
	return a;//这里的a是局部变量,返回别名,就类似于野指针了
}

const引用

只有引用和指针才会存在权限放大和缩小问题0

  • 可以引用一个const对象,但是必须用const引用,const引用也可以引用普通对象,因为权限可以缩小,但是不能放大
	const int a = 10;
//	int& ra = a;//权限放大,只读变成可读可写了,这不合理
	const int& ra = a;

	int b = 10;
	int& rb = b;
	const int& cb = b;//权限可以缩小		
	++b;//不影响原来的权限,是缩小引用的权限
	++cb;//这个不能修改
const int&  rc = 30;//const引用可以给常量取别名
int c = 2;
int d = 3;
const int& rd = (c + d);//表达式的值存在临时对象中,临时对象具有常性,所以要用const引用

注意看下面的代码:

const int a = 10;
int b = a;//这里不存在权限放大的问题,一定要注意,
int c = a + b;

在这里插入图片描述

double d = 12.34;
int i = d;//这里的d会隐式类型转换,中间会产生一个临时变量去存储
const int ri = d;//因为会产生临时变量,临时变量具有常性,所以要用const引用
//这里ri引用的是临时对象,不是d

指针和引用的关系

  • 语法上,引用时给变量取别名,不开空间,指针是存储一个变量的地址,要开空间

在这里插入图片描述

我们看看汇编就可以,可以看到,指针和引用在底层是一样的,但是,我们平时只谈语法的层面。

  • 引用在定义的时候必须初始化,指针不是必须的,但是我们建议初始化
  • 引用在初始化引用一个对象后,就不能再引用其他对象;而指针可以改变指向的对象
  • 引用可以直接访问指向的对象,指针需要解引用才能访问到
  • sizeof中的含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下是4个字节,64位平台下是8个字节)
  • 指针很容易出现空指针和野指针的问题,引用很少出现,引用相对来说安全一些

引用也不是绝对安全

int* ptr = NULL;
int& rb = *ptr;//这个可以编译通过
rb++;

在这里插入图片描述

但是退出代码不是0,就说明出错了,这里就是空引用,就错了

inline

内联函数

  • 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,直接在调用的栈帧里面执行,可以提高效率
  • inline对于编译器而言就是一个建议,编译器可以不听,不同的编译器关于inline是否在调用处展开的情况不同,因为C++标准没有规定这个,inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline会被编译器忽略

编译器会觉得你不靠谱,万一你写一个递归很深的函数,那展开不就会代码膨胀吗,编译器长大了,有自己的想法(doge

  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现是很复杂而且容易出错的,且不方便调试,C++设计inline的目的就是为了替换宏

如果要实现两个数相加的宏,你会怎么写?

//宏实现常见问题
//#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))
//为什么不加分号?-->避免语法错误
//为什么要加外面的括号?-->控制优先级
//为什么要加里面的括号?-->控制优先级
int main()
{
	int ret = ADD(1, 2);
	cout << ADD(1, 2) << endl;//加了分号就是cout << ADD(1, 2);<< endl;就会报错
	cout << ADD(1, 2) * 5 << endl;//->1 + 2 * 5,所以要加外面的括号
	int x = 1, y = 2;
	ADD(x & y, x | y); // -> (x&y+x|y), &和|的优先级没有+高
	return 0;
}

所以,使用宏是非常麻烦的,我们写成一个内联函数就会很方便

inline int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int ret = Add(1, 2);
	cout << Add(1, 2) * 5 << endl;
	cout << ret << endl;

	return 0;
}
  • VS编译器debug版本下默认是不展开inline的,因为方便调试,debug版本想要展开需要设置一下以下两个地方

在这里插入图片描述
在这里插入图片描述

环境设置为debug x32

  • inline不建议声明和定义分离到两个文件,分离会导致链接错误,因为inline被展开,就没有函数地址,链接时会报错
//F.h
#pragma once
#include <iostream>
using namespace std;

inline void f(int a);
//F.cpp
#include "F.h"//这就有内联属性,就不会把f函数的地址放进符号表

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

内联直接放到.h文件就行,不要分离到两个文件

记住,频繁调用的短小函数,适合内联

nullptr

空指针,我们在C语言中使用的是NULL,NULL实际上是一个宏,在传统的C头文件(<stddef.h>)中,可以看到如下代码:

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

但这个NULL有坑

void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0);
	f(NULL);//这该调用谁?-->第一个,因为NULL是0
	return 0;
}

这时候就会存在歧义,那我可以用强转解决吗?

f((void*)0);//不行,因为void*传这两都不匹配,就需要走隐式类型转换,那隐式类型转换成谁呢?int还是int*?所以还是有歧义

C语言倒是没问题,因为C语言允许void*隐式类型转换成任何类型的指针,但是C++不允许,必须强转

void* a = NULL;
int* b = a;//C语言可以这样写
int* b = (int*) a;//C++必须这样写

所以C++引入了nullptr,它可以隐式类型转换为任何类型的指针,但是,不能转换为整型

int* p1 = nullptr;
int p2 = nullptr;//就这个不行

简单来说,在C++中,我们就不要用NULL了,使用nullptr,可以解决空的问题。
id f(int a)
{
cout << a << endl;
}


内联直接放到.h文件就行,不要分离到两个文件

记住,**频繁调用的短小函数**,适合内联

## nullptr

空指针,我们在C语言中使用的是NULL,NULL实际上是一个宏,在传统的C头文件(<stddef.h>)中,可以看到如下代码:

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

但这个NULL有坑

void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0);
	f(NULL);//这该调用谁?-->第一个,因为NULL是0
	return 0;
}

这时候就会存在歧义,那我可以用强转解决吗?

f((void*)0);//不行,因为void*传这两都不匹配,就需要走隐式类型转换,那隐式类型转换成谁呢?int还是int*?所以还是有歧义

C语言倒是没问题,因为C语言允许void*隐式类型转换成任何类型的指针,但是C++不允许,必须强转

void* a = NULL;
int* b = a;//C语言可以这样写
int* b = (int*) a;//C++必须这样写

所以C++引入了nullptr,它可以隐式类型转换为任何类型的指针,但是,不能转换为整型

int* p1 = nullptr;
int p2 = nullptr;//就这个不行

简单来说,在C++中,我们就不要用NULL了,使用nullptr,可以解决空的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值