C++深入学习part_1

Linux下编译C++程序

安装g++命令:sudo apt install g++

编译命令:$ g++ *.cc 或者 *.cpp -o fileName;

hellworld

C++深入学习part_1_第1张图片

编译程序可以看到:
C++深入学习part_1_第2张图片

namespace命名空间

首先,命名空间的提出是为了防止变量重名冲突而设置的。
浅浅试一下:
C++深入学习part_1_第3张图片
现在我们进行编译的时候会发现报错:
C++深入学习part_1_第4张图片
从报错提示可以看到,他希望我们使用wd::display()的方式来调用该函数。(::称为作用域限定符)
这是因为display函数是定义在namespace命名空间里的,所以想要使用其内置成员的时候我们需要加上其对应的空间名:
C++深入学习part_1_第5张图片
再来编译:
在这里插入图片描述
完美运行通过。

命名空间还可以嵌套使用:
C++深入学习part_1_第6张图片
完美运行:
C++深入学习part_1_第7张图片
这是命名空间的一种使用方式,还有一种则是如下:
C++深入学习part_1_第8张图片
使用了using编译指令之后就可以不用再带对应的命名空间名了,因为上图中using编译指令会将std该空间的所有实体全部引入。

注意:第二种方式使用时必须知道该空间中有哪些实体,如果不知道,这样的写法就依然存在可能造成冲突的风险。

什么意思?
我们来试一下,这里使用std标准命名空间测试:
C++深入学习part_1_第9张图片
可以看见我们的cout函数具有二义性,因为std中也有一个该函数,编译会报错:
C++深入学习part_1_第10张图片
ambiguous:二义性。

所以初学时大型项目里面最好不要使用using编译指令,因为有可能造成冲突(你并不知道std中有多少函数)。

推荐使用using声明机制,即:using std::cout; 它只引入这一个实体。
另外在命名空间中直接定义的实体,不要直接缩进。

匿名命名空间

其实就是不带空间名就是匿名的命名空间,匿名命名空间可以直接使用其内部定义的实体。

#include 
using namespace std;
//匿名命名空间
namespace {

        int number = 4;

}

int main(void){


        cout << number << endl;

        return 0;
}

我们看存在的一种情况:

#include 
using namespace std;
//匿名命名空间
namespace {

        int number = 4;

}

int number = 5;

int main(void){


        cout << number << endl;

        return 0;
}

此时不出意料肯定会报错,因为具有二义性:
C++深入学习part_1_第11张图片
所以我们为了强调我们用的是哪个number,就需要使用匿名空间的作用域操作符:

#include 
using namespace std;
//匿名命名空间
namespace {

        int number = 4;//而这个变量只能在本模块内部使用

}

int number = 5; //该全局变量可以跨模块使用,即可以在另一个.cpp文件中使用
//所谓模块:一个*.c/*.cc/*.cpp的文件就可以叫做一个模块

//同理:
static int s_number = 5; //也只能在本模块内部使用

int main(void){
    
        //使用作用域操作符来使用匿名命名空间
        cout << ::number << endl;

        return 0;
}
              

运行就没有问题了,因为我们强调了使用命名空间中的number:
在这里插入图片描述

跨模块调用extern关键字

当我们要跨模块调用另一个cpp文件中的变量或者函数时,需要使用extern关键字。
我们在hello.cpp文件中写上g_number = 100;

#include 
using namespace std;

//等待被namespace1.cpp调用的变量
int g_number = 100;

//这里要注意嗷,我们上面的所谓跨模块调用意思是这些模块本身就属于同一个项目
//而一个项目只能有一个main函数,所以这里我们注释掉hello.cpp中的main函数
//int main() {

  //      cout << "hello world" << endl;
    //    return 0;
//}

然后我们在namespace1.cpp文件中通过extern关键字来引用它:

#include 
using namespace std;
//匿名命名空间
namespace {

        int number = 4;//而这个变量只能在本模块内部使用

}

int number = 5; //全局变量可以跨模块使用,即可以在另一个.cpp文件中使用

//使用hello.cpp文件中的g_number变量
extern int g_number;

int main(void){

        //使用作用域操作符来使用匿名命名空间
        cout << ::number << endl;
        
        //打印g_number
        cout << g_number << endl;

        return 0;
}

编译运行:
C++深入学习part_1_第12张图片

另外,在同一个模块中可以定义多次命名空间;在不同的模块中也可以定义多次命名空间:

#include 

using namespace std;


//第一次定义命名空间wd
namespace wd{

        void show(); //这里是第一次声明实体show()
}


int main(){

        //调用wd中的show()
        wd::show();

        return 0;
}


//这里我们第二次定义命名空间wd
namespace wd{

        //第二次定义实体show
        void show();
}

//这里我们第三次定义命名空间wd
namespace wd{

        //第三次声明并且定义实体show
        void show(){
                cout << "我是第三次被声明啦" << endl;
        }
}

注意虽然命名空间可以随便声明,但是它里面的函数声明可是只能有一次定义嗷(就和正常的函数一样)。
编译运行:
C++深入学习part_1_第13张图片
不光是本文件可以重复声明命名空间,跨文件(或者说跨模块)也一样可以,这里我们在namespace3文件中定义一个同名wd:

#include
using namespace std;


//在namespace3.cpp文件中定义重名namespace wd
namespace wd
{
        void print(){
                cout << " 我是跨模块的命名空间嗷  " << endl;
        }
}

然后我们返回到刚刚的测试文件中去调用它:


#include 

using namespace std;


//在这里调用跨文件的namespace wd
namespace wd{

        void print();
}

//第一次定义命名空间wd
namespace wd{

        void show(); //这里是第一次声明实体show()
}


int main(){

        //调用wd中的show()
        //wd::show();

        //调用跨文件的namespace3.cpp中的wd
        wd::print();
        return 0;
}


//这里我们第二次定义命名空间wd
namespace wd{

        //第二次定义实体show
        void show();
}

//这里我们第三次定义命名空间wd
namespace wd{

        //第三次声明并且定义实体show
        void show(){
                cout << "我是第三次被声明啦" << endl;
        }
}

编译运行:
在这里插入图片描述

总结:
1、命名空间的提出是为了防止变量重名冲突而设置的,可以嵌套使用
2、去除了命名空间名就是所谓的匿名命名空间,匿名命名空间的实体无法跨模块调用。
3、在同一个模块中可以定义多次命名空间,在不同的模块中也可以定义多次命名空间。

const修饰类型和对象

const:修饰类型或对象成为常量值的关键字,常量值不可以改变且必须初始化。

#include 

using namespace std;

#define MAX 1000

void test(){
        int a;
        //const int b; 必须要继续初始化,否则报错
        const int c = 1;
        //c = 2; error 常量是不能进行修改的
        
        //有同样效果的还有宏定义#define
        cout << MAX << endl;
}

int main(){
        
        test();

        return 0;
        
}

宏定义与const常量的区别(面试常考)

1、发生的时机不一样:
宏定义是在预处理时,而const常量是在编译时
2、类型检查不一样:
宏定义是没有类型检查的,只是简单做了字符串的替换,虽然也有编译阶段,但在编译阶段没有报错,将出错的时机推迟到了运行时,但运行时阶段的错误是更难发现的;而const是由类型检查的,这样更加安全一些

那么什么叫宏定义只是作了字符串的简单替换呢?
这里我们举例说明:

#include 

using namespace std;

//举例说明为什么宏定义只是进行了简单的字符串替换
#define kBase 3+4

void test(){
        int a = 10;
		
        int d = a * kBase;
		
        cout << "d: " << d << endl;
}

int main(){
        
        test();

        return 0;
        
}

上面代码理想的值应该是得到10 * (3 + 4) = 70,但编译运行结果为:
在这里插入图片描述
我们可以用如下命令去查看预处理阶段的代码长什么样:
在这里插入图片描述
上面的constL.cpp和constL.i都是文件名,.i文件就是我们的预处理文件,打开它可以看见:
C++深入学习part_1_第14张图片

3+4被当作字符串一样直接替换了kBase,所以最后的结果就成了10*3+4-34。

总结:要定义常量时最好使用const或者枚举enum类型。

const修饰指针

# include 

using namespace std;

int main(){

        int a = 10;

        //这种形式是常量指针,表示p1所指向的a对象的值不可以改变
        //即p1也可以指向别人,如p1 = &b;
        //但(*p1) = 20; 企图修改a的值就是错误的,该值不可改变
        const int* p1 = &a;

        //int const* p2 = &a; 这种格式和上面p1指针是一个意思,且不怎么用

        //这种形式是指针常量,表示p3所指的地址值不可以改变
        //即p3不可以指向别人了,如p3 = &b; 就是错误的,指向不可改变
        //而(*p3) = 30; 这是可以的,其所指对象的值可以改变
        int* const p3 = &a;
}

C++堆空间申请方式以及内存泄露

C语言中申请堆内存空间使用的是malloc和free函数。
在C++中也有类似的方式:new表达式与delete表达式。

int * pint = new int(10); //new表达式申请空间的同时,也进行了初始化

delete pint; //释放申请的堆空间

简单尝试:

#include 

using namespace std;


int main(){

        //new表达式执行完毕之后,返回的是相应类型的指针
        int* pint = new int(1);

        cout << "*pint  = " << *pint << endl;
}

运行编译:
在这里插入图片描述
但此时我们的代码是有问题的,因为没有释放掉我们的pint空间,即发生了内存泄漏。
那我们怎么检测我们的程序是否发生了内存泄露呢?
答案是使用一些内存检测工具,比较常用的如:valgrind
执行下面的命令下载它:
在这里插入图片描述

内存泄露检测工具-valgrind(面试高频考点)

下载完毕后我们执行以下操作:
C++深入学习part_1_第15张图片

上面的操作结束后现在我们就可以直接使用别名命令memcheck来检测内存泄露了,我们来检测一下我们刚刚的内存是否存在泄露:
C++深入学习part_1_第16张图片
从in use at exit:4bytes in 1 blocks等信息中可以看出,存在内存泄露问题,着就是内存检测工具valgrind的简单使用。

所以要记得回收内存啊!

关于new还有一种使用方式:

#include 

using namespace std;


int main(){

        //1、第一种new的使用方式
        //new表达式执行完毕之后,返回的是相应类型的指针
        int* pint = new int(1);

        cout << "*pint  = " << *pint << endl;

        delete pint;

        //2、第二种new的使用方式
        //new表达式要申请的空间为数组
        int* parr = new int[10];
        //注意数组的堆空间申请和释放的语法嗷,中括号别掉
        delete[] parr;

}

引用

C++深入学习part_1_第17张图片

引用作为函数参数传递

首先回忆一下之前的几种参数传递方式:

1、值传递

还是使用经典的交换两个变量值的内容来作例子:
C++深入学习part_1_第18张图片
由上图可知,在使用值传递时,其实传递的仅仅是变量值的拷贝,而我们建立的在swap函数中定义的tmp值也不过是个临时变量,当swap函数执行结束后tmp变量也就随即消失了。所以值传递并不能实现交换两个值的内容,这是由于两个函数并不共享一块内存空间决定的,虽然它们都存在在栈空间内。
注意图中的箭头仅仅意味着进行了一个参数的拷贝而已,即a1的值拷贝给了变量x。

2、地址传递

C++深入学习part_1_第19张图片
地址传递就不一样了,上图明显可以看见指针px指向了a的地址,指针py指向了b的地址。
所以对两个指针解引用可以得到:*px = 1; *py = 2
在swap函数中,第20行代码tmp暂存了 *px的值,然后第二十一行中,px所指地址空间上的值被修改成了 *py的值,即 *px = 2;
第22行代码则将 *py的值变成了 1。
所以达到了我们想要交换两个变量值的目的:
C++深入学习part_1_第20张图片

3、引用传递

C++深入学习part_1_第21张图片
这就没啥好说的了,因为引用其实就是别名,所以操作x和y其实就是在直接操作a和b罢了。

引用的出现就是为了替代指针,尽量让程序员减少犯错的概率。
其底层实现依然是指针,而且是一个受限制的指针,即引用一旦被绑定到某个对象上后就不能够解绑去绑定到别的对象上。

引用作为函数的返回值

C++深入学习part_1_第22张图片
C++深入学习part_1_第23张图片

强制转换与函数重载

C风格强制转换:

TYPE a = (TYPE) EXPRESSION;

但这种风格存在缺陷,就是安全性不足,无法保证转换之后类型的合法性。

而C++风格的强制转换就不一样了

有四种:
1static_cast(最常用,比如常见的指针转换:把void*转换成其它类型的指针)

2const_cast(去除常量属性)

3dynamic_cast(动态类型转换,只用在多态时基类与派生类之间的转换)

4reinterpret_cast(在任意类型之间轻易转换,但是不要轻易使用,用的最少)

上面四种转换方式只是含义不一样,但写法是通用的,形式都如下:
在这里插入图片描述

static_cast

这个没什么好讲的,就是正常用就型:

int* p = static_cast<int*> (malloc(sizeof(int)));

上面这一句代码就是一个很典型的应用,在C风格中malloc函数返回的是void*,如果要使该行代码不报错的话,就必须进行类型转换,那么此时用static_cast是非常合适的。

const_cast


#include 

using namespace std;


void display(int* p){ //明显要求传入一个非const指针

        *p = 10;
        cout << "*p = " << *p << endl;
        
}

int main(){

        const int a = 1;

        display(&a);//在实参传递时,只有const变量,如果传递成功的话就有修改a的值的风险

}

如上面注释所说,这肯定是报错的:
C++深入学习part_1_第24张图片
那如果我们一定要传这个const常量参数呢?
那就可以用上const_cast了:

#include 

using namespace std;


void display(int* p){ //明显要求传入一个非const指针

        *p = 10;
        cout << "*p = " << *p << endl;

}

int main(){

        const int a = 1;

        //进行了去除const的强制类型转换
        display(const_cast<int*> (&a));

}


现在再运行就没有问题了:
C++深入学习part_1_第25张图片
虽然我们使用了const_cast进行转换,但是我们并没有真正改变const变量的值,这一点要注意,即上面代码中的a的值是没有变化的。

而且更有意思的是,当我们打印指针p的值(即变量a的地址)的时候会发现,它的地址居然和常量a是一模一样的:

#include 

using namespace std;


void display(int* p){ //明显要求传入一个非const指针

        *p = 10;
        cout << "*p = " << *p << endl;
        cout << "p所指地址为: " << p << endl;
}

int main(){

        const int a = 1;

        //进行了去除const的强制类型转换
        display(const_cast<int*> (&a));
        
        cout << "a的地址为: " << &a << endl;

}

运行结果:
C++深入学习part_1_第26张图片
这就很扯:地址值是一样的,但是值不一样。

所以迷惑性很强,一般情况下最好不要用const_cast。(这里很多资料里面都没有一个明确的说法,据说是*p的值存在了所谓的寄存器中,并没有真正写入内存啥的,反正知道有这么回事就行了)

dynamic_cast和reinterpret_cast两个就不讲了,基本用不到

函数重载

C++深入学习part_1_第27张图片

C语言不支持函数重载!

C++深入学习part_1_第28张图片
C++深入学习part_1_第29张图片
由上图可以发现,确实对于不同的重载函数其实就是改变一下对应的名字来调用而已。add是函数名,然后add后面的ii就是参数列表中各个参数的缩写。

C++与C的混合编程

上一节我们知道了C++在内部是使用了名字改编的原理来支持函数重载的,但是C语言不支持函数重载自然也就没有所谓名字改编的操作,这就导致了C和C++在进行混合编程的时候会出现一些兼容问题:

C++深入学习part_1_第30张图片

为什么需要进行混合编程:很明显,C比C++早十二年出来,很多库都是C写的,C++只能去兼容和适应C的法则;

为了解决这样的问题,我们引入了下面的方式来解决兼容性问题:

C++深入学习part_1_第31张图片
上图右侧就是混合编程的编译结果,示例代码如下:

#include 

using namespace std;

//用C语言的方式来调用该函数
extern "C"{

        //只要放在该区域的代码,就会按照C的方式进行调用
        //不会进行名字改编
        int add(int x, int y){
                return x + y;
        }
}// end of extern "C"


//下面这些重载函数都是按C++方式来进行调用的
long add(long x , long y){
        return x + y;
}

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

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

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

int main(){

        return 0;
}

上述这是 extern "C"声明在实现文件.cpp文件中的情况,但是如果是在头文件中情况又当如何呢?

在头文件中,文件是有可能被C编译器编译的,也有可能是被C++编译器编译的,自然的说C编译器肯定是不需要上面那段extern ''C"就能编译,会节省时间,而C++编译器则需要这段代码,如何做才能得到这样的效果?

答案是使用C++中的条件编译,宏定义:
C++深入学习part_1_第32张图片
所以在头文件中加上上述内容:

//宏_cplusplus只有C++的编译器才会定义
//C的编译器没有该宏
//意思就是只有该被包围起来的代码是被C++编译器编译时才会出现
//若是被C编译器编译的话就不会出现
#ifdef _cplusplus
extern "C"
{
#endif
        
        int add(int x,int y){
                return x + y;
        }
#ifdef _cplusplus
}
#endif

通过上述方法就可以完美解决C与C++的混合编程问题。

默认参数

C++深入学习part_1_第33张图片
这其实没啥好说的,就注意一下上面说的一个点:
默认参数的设置要求必须从右到左进行;
另外设置默认参数的时候要注意是否有其它的重载函数与其设置了默认参数的参数列表产生调用时的二义性就行。

inline函数

首先在C语言中,其实有类似的语法,函数宏定义:

#include 

using namespace std;


//C语言中的函数宏定义
#define multiply(x,y) x * y


int main(){
        int a = 3, b = 4;
        int c = 5, d = 6;
        cout << multiply(a,b) << endl;//输出为12
        //但还是之前的问题,宏定义只是简单替换成了字符串
        //所以下面的语句其实是:a+b*c+d = 29
        cout << multiply(a+b,c+d) << endl;
}

编译运行:
C++深入学习part_1_第34张图片
接下来我们看在C++中有同样功能的inline函数:

#include 

using namespace std;


//C语言中的函数宏定义
#define multiply(x,y) x * y

//C++中的inline函数
//为什么有inline函数:就是因为每次函数的调用都是绝对有开销的(比如栈空间的消耗)
//那么加上inline关键字的话,在编译时编译器会将该函数进行语句的替换
//下面的函数调用就会被替换成语句 x / y,极大的提升了效率
//它的效率与宏函数保持一致,还更加安全

inline int divide(int x,int y){
        
        return x / y;
}

int main(){
        int a = 3, b = 4;
        int c = 5, d = 6;
        cout << multiply(a,b) << endl;//输出为12
        //但还是之前的问题,宏定义只是简单替换成了字符串
        //所以下面的语句其实是:a+b*c+d = 29
        cout << multiply(a+b,c+d) << endl;

        //调用inline内联函数
        cout << divide(d,a) << endl;
}

为了降低犯错误的概率,尽量使用inline内联函数。

内联函数的使用要求

C++深入学习part_1_第35张图片

C++内存布局(面试常考)

C++深入学习part_1_第36张图片
上图是每一个进程被装载到内存中运行时的内存空间分布示意图,每个进程被装载到内存中运行时都会有如上几个区。
32位操作系统意思就是每一次读写数据的话只能读写32个位也就是四个字节,因为只有32根地址线来传送数据(所以32根地址线最大传送的数据就是当这32个位全为1的时候,最小就是当这三十二个位全为0的时候,这就决定了该类型操作系统的内存地址空间范围),那么2的32次方也就是4G大小的内存空间,其中一部分用来作OS的系统空间,即上图中的内核态,用来运行一些内核程序,而剩下的部分就是用户去区,也就是上图的用户态,用户进程(也就是我们所编写的程序)都会运行在用户态中。我们的C++程序也一样会运行在用户态里,只不过完整的程序根据其代码的不同会被分到不同的内存区域中,其中栈区总是位于虚拟地址的高位部分,向低地址方向进行生长,而堆区则在其下面由低地址向高地址生长,全局/静态区(或者说读写段)和只读段(或者说文字常量区和程序代码区)则依次往下存放。

接下来我们来一一验证,通过本次学习以后必须清楚自己写下的每一句代码中的数据是存储在哪个空间里的。

#include 

using namespace std;


int gNumber = 1;

static long sNumber = 2;

const int kNumber = 3;

void test(){

        //对于使用指针声明的字符串,该字符串位于文字常量区,声明时应该加上const否则会有警告
        //所以正确的声明应该是:const char* pstr = "hello,world";
        char* pstr = "hello,world";
        //*ptr = 'H'; 错误,因为文字常量区是只读区域,所以从侧面反映了其确实位于文字常量区


        //该字符串位于栈上,相当于用"hello,world"字符串去初始化了这个字符数组
        char pstr2[] = "hello,world";

        int number = 1;

        const int number2 = 1;

        const int* const p = &number2;

        static int sLocalNumber = 10;

        //pint本身位于栈上
        int* pint = new int(1); //堆区

        delete pint;

        printf("pstr: %p\n",pstr);
        printf("&pstr: %p\n",&pstr);
		/*这里要注意辨析一下pstr2和&pstr2的区别,虽然它们俩打印出来的地址是一样的
         * pstr2是指该字符数组的首个元素的地址,即&pstr2[0]的地址,我们通过对其+1可以拿到&pstr2[1]的地址,也可以访问其元素
         * 而&pstr2的意思则是取整个字符数组的地址,也就是首个元素的地址
         * 但此时对&pstr2+1的话我们不会拿到第二个元素的地址,反而是会把整个字符数组当作第一个元素,然后去访问下一个字符数组
         * 的元素,也就是偏移的是整个数组的长度而不是偏移一个元素的长度
         */
        printf("pstr2: %p\n",pstr2);
        printf("&pstr2: %p\n",&pstr2);
        //pstr2 = 0x11; 错误 数组名是一个常量,不能修改它的值
        printf("&gNumber: %p\n",&gNumber);//全局静态区
        printf("&sNumber: %p\n",&sNumber);//全局静态区
        printf("&number: %p\n",&number);//栈区
        printf("&number2: %p\n",&number2);//依然是放在栈上,因为该常量是定义在本函数内的,声明周期在本函数内
        printf("&kNumber: %p\n",&kNumber); //放在文字常量区,所谓文字常量意思是“字面常量“
                                           //包括数值常量、字符常量和符号常量
        printf("&sLocalNumber: %p\n",&sLocalNumber);//放在全局静态区

        //查看函数的地址
        //函数的名称即函数的入口地址存在于全局静态区,即程序存在它就存在
        //所以查看其地址时会发现函数地址和全局变量的地址相近
        //但是通过函数名去调用具体函数时就会在栈空间里了
        //所以函数内部的局部变量都是存放在栈空间上
        printf("&test: %p\n",&test);
}

int main(){
        test();
        //查看main函数的地址
        printf("&main: %p\n",&main);

}

编译运行:
C++深入学习part_1_第37张图片

自己可以对照着看看,加深一下理解,然后下面是对上面代码中提到的pstr2和*pstr2的区别图示:C++深入学习part_1_第38张图片

你可能感兴趣的:(C++学习,c++,学习)