C++基础知识

1. C++入门程序

//hello.cc
#include <iostream> //告诉编译器我们要使用iostream库,C++头文件在/usr/include/c++/..下都是用模板写的,在编译阶段具体化实现

using namespace std;//命名空间

/*注:
1.函数声明仅向编译器提供已给定类型和名称存在的变量的确定性;
2.函数定义为变量分配存储空间;
3.函数声明可以多次,函数定义只能有一次。
*/

//函数定义
int main(int argc, char** argv)//命令行参数
{
//函数体
	//cout,输出流对象
	//<<,输出流运算符
	//"Hello world",字符串字面值常量
	//endl = end of line,结束当前行,将缓冲区内容确实刷新到设备中
	//operator<<,运算符重载
	cout << "Hello world" << endl;//等价:operator(cout,"Hello world").operator<<(endl);

	int number = 0;
	//cin,输入流对象
	//>>,输入流运算符	
	cin >> number;//等价: cin.operator>>(number);
	cout << "number = " << number << endl;
	
	return 0;//return结束函数执行,返回值与函数返回类型相容,main的返回值常用来指示状态,0表明成功
}

1.1 源文件命名约定

在大多数系统中,源文件名字是以.后接一个或多个字符组成的后缀结尾,从而告诉系统这是一个C(.c)C++(.cc或.cpp)Java(.java)Python(.py或.pyc)程序。

1.2 C/C++可执行文件的生成

预处理
编译
汇编
预处理
编译
汇编
预处理
编译
汇编
链接
源文件1
预处理后的文件1.i
汇编代码1.s
目标文件1.o
源文件2
预处理后的文件2.i
汇编代码2.s
目标文件2.o
.........
源文件n
预处理后的文件n.i
汇编代码n.s
目标文件n.o
引导文件
库文件
链接器
可执行文件

编译过程分为四步:

  • 预处理:读取源代码并对其中的以#开头的指令和特殊符号进行处理。详细处理内容如下:
    1.#include头文件包含,将头文件的内容copy到预处理指令所在位置(< …>系统目录、"…"当前同一目录);
    2.#define 简单文本替换,宏定义以及宏函数(带参数的宏),使用宏函数注意:左括号要紧贴宏函数的名称、把整个宏函数的表达式用括号括起来、将参数用括号括起来、警惕参数导致的多次副作用;
    3.处理条件编译指令,如:

    • #undef 取消已定义的宏;
    • #if 如果给定条件为真,则编译下面代码;
    • #ifdef 如果宏已经定义,则编译下面代码;
    • #ifndef 如果宏没有定义,则编译下面的代码;
    • #elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码,是else if的简写;
    • #endif 结束一个#if …#else条件编译块。

    4.将那些不必要的代码过滤掉,如注释;
    5.保留所有的#pragma编译器指令并添加行号和文件标识,这样编译时编译器就会产生调试、出现错误和警告时的行号信息等。

//C使用
gcc -E test.c -o test.i

//C++使用
g++ -E test.cc -o test.i

源程序与预处理后的程序

  • 编译:将预处理后的文件进行词法、语法、语义分析和优化,template(函数模板和类模板)在编译阶段完成具体化,inline(内联)在编译阶段将调用函数的代码替换为函数本体,以减少开销。
//C使用
gcc -S test.i -o test.s

//C++使用
g++ -S test.i -o test.s

可在编译时进行O1、O2、或O3优化(级别由低到高),C语言为例:

gcc -S test.i -01 -o test.s
gcc -S test.i -02 -o test.s
gcc -S test.i -03 -o test.s
  • 汇编:将汇编代码test.s转换成二进制机器码(目标文件)test.o。
//C使用
gcc -c test.s -o test.o

//C++使用
g++ -c test.s -o test.o
  • 链接:将多个目标文件以及所需的库函数(静态库xxx.a或xxx.lib,动态库xxx.so或xxx.dll)链接成最终的可执行文件。
//C使用
gcc test.o -o test.out

//C++使用
g++ test.o -o test.out

//执行预处理、编译、汇编和链接,生成可执行程序,默认a.out
g++ test.cc

//指认名称-o
g++ test.cc -o filename(自定义)

过程如下:
在这里插入图片描述

2. 命名空间

2.1 为什么使用命名空间

大型的工程往往是由若干个人合作完成的,最后再组合成一个完整的程序。因为各个头文件是由不同的人设计的,可能存在不同的头文件中用了相同的名字来命名所定义的类或函数,这样在程序中就会出现名字冲突或者自己定义的名字会与C++库中的名字发生冲突。为了解决命名冲突 ,C++中引入了命名空间。

2.2 什么是命名空间

命名空间又称为名字空间,是程序员命名的内存区域,程序员根据需要指定一些有名字的空间域,从而与其他全局实体分隔开,互不干扰,系统能够区分它们。

  • 基本用法:
#include <iostream>

using namespace std;
int val = 10;
//命名空间
/*1.在命名空间中可以定义变量、常量、函数的定义或声明、结构体、类、模板以及命名空间的嵌套等等;
2.统称为实体,作用域是全局的,但可见区域并非全局的(从实体创建到名称空间结束);
3.在名称空间外,该实体不可见。*/
namespace name
{
int number = 10;
void print()
{
	cout << "void print()" << endl;
}
struct Foo
{
	char ch;
	int a;
};

}//end of namespace name
;//表明是一条空语句

int main(int argc, char* argv[])
{
    cout << "val = " << val << endl;
    /*报错:cout << "number = " << number << endl;*/
    //命名空间中名字的访问:命名空间名+作用域限定符::+名字
    cout << "number = " << name::number << endl;
    cout << "Hello world" << endl;
    /*报错:print();*/
    name::print();
    
    return 0;
}

运行过程和报错信息:
在这里插入图片描述

2.3 命名空间使用方式

2.3.1 using编译指令

优点:如果一个名称空间中有多个实体,使用using编译指令,可以将该空间中的所有实体一次性全部引入到程序之中;
缺陷:对于初学者来说,因为不熟悉命名空间中的实体,有可能还是会造成同名字冲突的问题,当然该错误是人为造成的,如下代码:

#include <iostream>

using namespace std;//1、using编译指令,将标准命名空间std中的实体全部引出来

namespace name
{
int number = 10;
void print()
{
	cout << "print()" << endl;
}
}//end of namespace name

//缺陷:自定义的实体与标准命名空间std中的实体冲突
void cout()
{

}

int main(int argc, char* argv[])
{
    //命名空间中名字的访问:命名空间名+作用域限定符::+名字
    cout << "number = " << name::number << endl;
    cout << "Hello world" << endl;
    name::print();

    cout();

    return 0;
}

2.3.2 作用域限定符

优点:即使与其他命名空间中的实体“冲突”了,也保证代码正确;
缺陷:代码书写的时候,相对麻烦,如下代码:

#include <iostream>

namespace name
{
int number = 10;
void print()
{
	std::cout << "print()" << std::endl;
}
}//end of namespace name

//2、作用域限定符的形式
int cout(int x, int y)
{
    std::cout << "x = " <<
    out << "y = " << y << std::endl;
    return x+y;
}

int main(int argc, char* argv[])
{
    //命名空间中名字的访问:命名空间名+作用域限定符::+名字
    std::cout << "number = " << name::number << std::endl;
    std::cout << "Hello world" << std::endl;
    name::print();

    cout(4,5);

    return 0;
}

2.3.3 using声明机制

综合了using编译指令与作用于限定符两种方式的优缺,比较推荐!代码如下:

#include <iostream>

using std::cout;//3、using声明机制,一次只引出一个实体
using std::endl;

namespace name
{
int number = 10;

void print()
{
    cout << "print()" << endl;
}

}//end of namespace name

//不引出,就没关系,不会产生冲突
int cin(int x, int y)
{
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    return x+y;
}

int main(int argc, char* argv[])
{
    //命名空间中名字的访问:命名空间名+作用域限定符::+名字
    cout << "number = " << name::number << endl;
    cout << "Hello world" << endl;
    name::print();

    cin(4,5);

    return 0;
}

2.4 命名空间的扩展

#include <iostream>

using std::cout;
using std::endl;

/*结构体是不能重新定义的,不能进行扩展的*/
#if 0
struct MyStruct
{
char ch = 'A';
};

struct MyStruct
{
int number = 10;
};
#endif

//1、带命名空间的函数声明,自定义命名空间是可以进行扩展的;
namespace name1
{
void show();
}//end of namespace name1

namespace name1
{
char ch = 'A';
}//end of namespace name1

namespace name
{
int number = 10;

void print()
{
    cout << "void print()" << endl;
    name1::show();
}

}//end of namespace name

namespace name1
{
int number = 10;

void show()
{
    cout << "void show()" << endl;
}

void display()
{
    name:: print();
}

}//end of namespace name1

//2、标准命名空间也是可扩展的,所有命名空间都是可扩展的。
namespace std
{

struct MyStructA
{
    int a = 100;
    char b = 'B';
};

}//end of namespace std

int main(int argc, char* argv[])
{
    name1::display();

    cout << "===========" << endl;

    name::print();
    return 0;
}

2.5 匿名命名空间

作用域:命名空间还可以不定义名字,不定义名字的命名空间称为匿名命名空间。由于没有名字,该空间中的实体,其它文件无法引用,它只能在本文件的作用域内有效,它的作用域是从匿名命名空间声明开始到本文件结束。
可见域:在匿名空间中创建的全局变量,具有全局生存期,却只能被本空间内的函数等访问,是static变量的有效替代手段。

namespace {
int number = 10;
void func();
}//end of anonymous namespace

2.6 命名空间的嵌套

#include <iostream>

using std::cout;
using std::endl;

//作用域与可见域,由于会出现屏蔽现象,所以一般作用域>可见域
int number = 20;
int a = 200;
namespace name
{
int number = 10;

void print(int number)
{
    cout << "形参number = " << number << endl;
    cout << "命名空间name中number = " << name::number << endl;

    //匿名命名空间
    cout << "全局变量number = " << ::number << endl;//存在名字冲突,加::
    //兼容C
    cout << "a = " << a << endl;//独一份,无冲突,加不加::没关系
    cout << "a = " << ::a << endl;
    
    printf("hello world\n");//独一份,无冲突,加不加::没关系
    ::printf("hello world\n");
}

}//end of namespace name

namespace name1
{
int number = 100;

void print()
{
    cout << "void name1::print()" << endl;
}

//命名空间的嵌套
namespace name2
{
int number = 200;
}//end of namespace name2

}//end of namespace name1

int main(int argc, char* argv[])
{
    name::print(30);
    cout << "number = " << name1::name2::number << endl;
    return 0;
}

3. const关键字的用法

3.1 const修饰普通变量

#include <iostream>

using std::cout;
using std::endl;

//宏定义创建常量
//宏定义发生的时机在预处理阶段,进行简单文本替换,无类型检查
//如果出现bug,可能需要等到运行的时候才知道
#define MAX 10

void test()
{
    int a;
    //const修饰的变量称为常量
    //const常量发生的时机在编译的阶段,出现bug(编译错误--语法错误),编译阶段即可知道
    //const既可以放在内置类型的前面也可以放在内置类型的前面
    //内置类型:char/short/int/float/double/long/bool,与后面面向对象中的自定义类型区分
    /* const int number;//error,常量在定义的时候必须要进行初始化 */
    const int number = 10;//等价于 int const number = 10;
    cout << "number = " << number << endl;

    /* number = 20;//error,常量不能进行赋值,为了保护常量不被修改 */
    cout << "number = " << number << endl;

}
int main(int argc, char** argv)
{
    test();
    return 0;
}

3.2 const修饰指针

#include <iostream>

using std::cout;
using std::endl;

void test2()
{
    //搞清:1.指针所指变量的值、2.指针的指向
    int number = 10;
    int *p = &number;
    //number = 20;
    *p = 20;
    cout << "*p = " << *p << endl;
    cout << "number = " << number << endl;

    int a = 20;
    p = &a;
    p = NULL;//C (void *)0, C++ 0
    p = nullptr;//C++11,空指针的一个用法,C++,nullptr,(void*)0

    cout << "==========" << endl;
    int value1 = 100;
    //1.当const位于*左边的时候,称为常量指针
    const int *p1 = &value1;//等价于 int const *p1 = &value;
    /* *p1 = 10;//error,不能改变指针所指的变量的值 */
    p1 = &number;//ok,可以改变指针本身(指向)
    cout << "*p1 = " << *p1 << endl;
    cout << "value1 = " << value1  << endl;
    
    cout << "==========" << endl;
    int value2 = 200;
    //2.当const位于*右边的时候,称为指针常量
    int * const p2 = &value2;
    *p2 = 20;//ok,可以改变指针所指的变量的值 */
    /* p2 = &number;//error,不能改变指针本身(指向) */
    cout << "*p2 = " << *p2 << endl;
    cout << "value2 = " << value2 << endl;
    
    cout << "==========" << endl;
    int value3 = 300;
    //3.双const
    const int * const p3 = &value3;
    /* *p3 = 30;//error,不能改变指针所指的变量的值 */ 
    /*  p3 = &number;//error,不能改变指针本身(指向) */ 
    cout << "*p3 = " << *p3 << endl;
    cout << "value3 = " << value3 << endl;
}

int main(int argc, char* argv[])
{
    test2();
    return 0;
}

/*理解:两组重要的概念
	数组指针					指针数组/int类型的数组int pArray[]
	int (*pArray)[]			int* pArray[]

	函数指针					指针函数
	int (*pf)(int)			int* pf(int)
	
				int pf(int)
				{
				}
*/

4. new/delete表达式

#include <stdlib.h>
#include <string.h>
#include <iostream>

using std::cout;
using std::endl;

void test1()
{
    int* pInt  =  (int *)malloc(sizeof(int));//1、申请空间  
#if 0
    if(pInt == nullptr)
   {

   }
   else
   {
       //...
   }

#endif
   memset(pInt,0,sizeof(int));//2、初始化(清零)
   *pInt = 10;//3、操作(赋值)
   free(pInt);//4、回收空间
   pInt = nullptr;//5、防止野指针
#if 0
   if(nullptr == pInt)
   {

   }
   else
   {
       //...
   }
#endif
   
    //C申请堆空间数组
    int *pArray = (int *)malloc(sizeof(int)*10);//1、申请空间
    memset(pArray,0,sizeof(int)*10);//2、初始化(清零)
    pArray[0] = 10;//3、操作(赋值)
    pArray[1] = 20;
    free(pInt);//4、回收空间
    pInt = nullptr;//5、防止野指针
}

void test2()
{
    int* pInt = new int(10);//1、申请空间,然后初始化,并且赋值
    //...
    delete pInt;//2、回收空间
    pInt = nullptr;//3、回收野指针
#if 0
   if(nullptr == pInt)
   {

   }
   else
   {
       //...
   }
#endif
   
    //C++申请堆空间数组
    int *pArray = new int[10]();
    pArray[0] = 10;
    pArray[1] = 20;

    delete [] pArray;//回收堆空间
    pArray =  nullptr;//防止野指针
}
int main(int argc,char* argv[])
{
    test1();
    test2();
    return 0;
}

  • 深度理解回收堆空间原理点击:link
  • Q1:new/delete表达式与malloc/free的异同点是?
    A1:
    不同点:
    1)malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符或表达式 ;
    2)new能够自动分配空间大小,malloc需要传入参数;
    3)new开辟空间的同时还对空间做了初始化的操作,而malloc不行;
    4)new/delete能对对象进行构造和析构函数的调用,进而对内存进行更加详细的工作,而malloc/free不能。既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
    相同点:
    1)都是用来申请堆空间的;
    2)都是需要成对出现(new/delete,malloc/free),否则会产生内存泄漏。
  • Q2:内存溢出?内存泄漏?内存踩踏?野指针?悬空指针?
  • A2 :
    内存溢出:指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足需求,就会报内存溢出的错误;
    内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏不断堆积后,就会造成内存溢出;
    内存踩踏(踩内存):访问了不合法的地址 ,通俗一点就是访问了不属于自己的地址,如果这块地址分配给了另一个变量使用,就会破坏别人的数据;
    野指针:未初始化的指针或者指向未知区域的指针,对野指针进行解引用运算时,会导致未定义行为;
    悬空指针:指针指向的对象被释放,但该指针没有任何变化,依然指向已回收的内存地址。

5. 引用reference

5.1 基本概念

#include <iostream>

using std::cout;
using std::endl;

void test()
{
    int number = 10;
    //1、引用是一个变量的别名,操作引用与操作变量本身是一样的;
    //2、引用在定义的时候,必须进行初始化,不能单独存在;
    //3、引用一经绑定,就不能改变指向(引用底层实现就是一个指针*const,指针常量);
    //4、C++中引入引用的目的,就是减少指针的使用。
    int &ref = number;
    cout << "number = " << number << endl;
    cout << "ref = " << ref << endl;
    printf("&number = %p\n",&number);
    printf("&ref = %p\n",&ref);

    cout << "=======" << endl;
    ref = 20;
    cout << "number = " << number << endl;
    cout << "ref = " << ref << endl;

    cout << "=======" << endl;
    number = 30;
    cout << "number = " << number << endl;
    cout << "ref = " << ref << endl;

    cout << "=======" << endl;
    int number1 = 100;
    ref = number1;
    cout << "number = " << number << endl;
    cout << "number1 = " << number1 << endl;
    cout << "ref = " << ref << endl;
    
    printf("&number = %p\n",&number);
    printf("&number1 = %p\n",&number1);
    printf("&ref = %p\n",&ref);

}
int main(int argc, char* argv[])
{   
    test();
    return 0;
}

Q:既然引用的底层实现是指针,那么区别和联系是什么?
A:

  • 相同点:
  1. 都是地址的概念;
  2. 指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
  • 区别:
  1. 指针是一个实体,而引用仅是个别名;
  2. 引用使用时无需解引用(*),指针需要解引用;
  3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
  4. 引用没有 const,指针有 const;
  5. 引用不能为空,指针可以为空;
  6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
  7. 指针和引用的自增(++)运算意义不一样;
  8. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

5.2 引用作为函数参数

#include <iostream>

using std::cout;
using std::endl;

void swap1(int x, int y)
{
    int tmp = x;
    x = y;
    y = tmp;
}
void test1()
{
    int a = 3, b = 4;
    cout << "值传递===副本、拷贝开销" << endl;
    cout << "交换前a = " << a << ",b = " << b << endl;
    swap1(a,b);
    cout << "交换后a = " << a << ",b = " << b << endl;
}

void swap2(int* px, int* py)
{
    int tmp = *px;
    *px = *py;
    *py = tmp;
}

void test2()
{
    int a = 3, b = 4;
    cout << "值传递===地址、直观性不好" << endl;
    cout << "交换前a = " << a << ",b = " << b << endl;
    swap2(&a,&b);
    cout << "交换后a = " << a << ",b = " << b << endl;
}

void swap3(int& x, int& y)
{
    //操作引用与操作变量本身是一样的
    int tmp = x;
    x = y;
    y = tmp;
}

void test3()
{
    int a = 3, b = 4;
    cout << "传引用、直观性好" << endl;
    cout << "交换前a = " << a << ",b = " << b << endl;
    swap3(a,b);
    cout << "交换后a = " << a << ",b = " << b << endl;
}
int main(int argc, char* argv[])
{
    test1();
    test2();
    test3();
    return 0;
}

5.3 引用作为函数的返回类型

#include <iostream>

using std::cout;
using std::endl;


int func()
{
    int number = 10;
    return number;//在执行return时候,会有拷贝操作
}

/*error,不要返回局部变量的引用,因为其生命周期已经结束了!
int& func()
{
    int number = 10;//栈区
    return number;
}
*/

//如果使用堆空间的话,可以进行自动内存回收,则如下写法没有问题!
int& getHeapData()
{
    int *pInt = new int(10);
    return *pInt;
}
void test1()
{
    int a = 3, b = 4;
    int c = a + getHeapData() + b;//内存泄漏(new的操作没有delete)
    cout << "c = " << c << endl;
    //内存泄漏处理手段:
    int &ref = getHeapData();
    cout << "ref = " << ref << endl;
    delete &ref;
}
//ok,返回引用的前提:实体的生命周期一定大于函数的生命周期
int arr[10] = {1, 2, 3, 4, 5, 6};
int& getIndex(size_t idx)
{
    return arr[idx];//函数的返回类型如果是引用的话,减少拷贝操作
}

void test()
{
    cout << "getIndex(0) = " << getIndex(0) << endl;
    getIndex(0) = 100;//等价:arr[0] = 100;
    cout << "getIndex(0) = " << getIndex(0) << endl;
#if 0
    &getIndex(0);//ok
    &func();//error
    func() = 20;//error
#endif
}
int main(int argc, char* argv[])
{
    test();
    test1();
    return 0;
}

6. 强制转换

6.1 static_cast

#include <stdlib.h>
#include <iostream>

using std::cout;
using std::endl;

void test()
{
    int iNumber = 10;
    float fNumber = 12.34;
    //早期版本的C++语言中,显示地进行强制类型转换包含两种形式:
    /* iNumber = (int)fNumber;//1、C语言风格的强制类型转换; */
    iNumber = int(fNumber);//2、函数形式的强制类型转换。
}

//static_cast的基本用法
void test1()
{
    int iNumber = 10;
    float fNumber = 12.34;
    //1、两种基本类型之间进行转换
    iNumber = static_cast<int>(fNumber);
    cout << "iNumber = " << iNumber << endl;

    //2、void* 和目标指针之间进行转换
    void* pret = malloc(sizeof(int));
    int* pInt = static_cast<int* >(pret);
    free(pInt);
    pInt = nullptr;
}
int main(int argc, char* argv[])
{
    test();
    test1();
    return 0;
}

6.2 const_cast

#include <iostream>

using std::cout;
using std::endl;

void test()
{
    const int number = 10;
    /*error,保护常量不被修改
    int *pInt = &number;
    pInt = 20;
    */

    //const_cast只可以去除常量属性,但再执行写操作就会产生未定义行为
    int *pInt = const_cast<int*>(&number);
    //未定义行为
    *pInt = 20;
    cout << "number = " << number << endl;
    cout << "*pInt = " << *pInt << endl;
    printf("&number = %p\n", &number);
    printf("pInt = %p\n", pInt);
}

int main(int argc, char* argv[])
{
    test();
    return 0;
}

对于未定义行为的理解link

运行结果:
在这里插入图片描述
在这里插入图片描述

7. 函数重载(overload)

定义:在同一个作用域中,函数的名字相同,但是函数的参数列表不一样(参数的顺序、参数的个数、参数的类型)。

  • C中实现:
#include <stdio.h>

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

int add(int x, int y, int z)
{
    return x+y+z;
}
int main()
{   
    int a = 3, b = 4, c = 5;
    printf("add(a,b) = %d\n",add(a,b));
    
    printf("add(a,b,c) = %d\n",add(a,b,c));
    
    return 0;
}

运行结果:在这里插入图片描述

  • C++中实现
#include <iostream>

using std::cout;
using std::endl;

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

#if 0
//函数重载与函数的返回类型没有关系,依靠返回类型不能区分开来
float add(int x, int y)
{
    return x+y;
}
#endif

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

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

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

float add(float x, float y)
{
    return x+y;
}

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

int main(int argc, char* argv[])
{

    int a = 3, b = 4, c = 5;
    cout << "add(a,b) = " << add(a,b) << endl;
    
    cout << "add(a,b,c) = " << add(a,b,c) << endl;
    
    return 0;
}

在这里插入图片描述

  • extern "C"与C/C++的混合编程
#include <iostream>

using std::cout;
using std::endl;

//C/C++的混合编程
#ifdef __cplusplus//正确,我们正在编译C++程序
extern "C"
{
#endif
//如果想将一个函数按照C的方式进行编译,可以使用extern "C"将其包含起来,
//当没有#ifdef _cplusplus...#endif时用nm命令剖析目标代码.o可知该函数add没有进行名字改编
int add(int x, int y)
{
    return x+y;
}

#ifdef __cplusplus
}
#endif

#if 0
//函数重载与函数的返回类型没有关系,依靠返回类型不能区分开来
float add(int x, int y)
{
    return x+y;
}
#endif

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

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

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

float add(float x, float y)
{
    return x+y;
}

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

int main(int argc, char* argv[])
{

    int a = 3, b = 4, c = 5;
    cout << "add(a,b) = " << add(a,b) << endl;
    
    cout << "add(a,b,c) = " << add(a,b,c) << endl;
    
    return 0;
}

8. 默认参数

#include <iostream>

using std::cout;
using std::endl;

//默认参数必须要从右向左进行连续赋值,C/C++中,函数参数入栈顺序:从右向左
int add(int x, int y = 1, int z = 0)
{
    return x + y + z;
}

//相当于如下三个函数,默认参数的使用就是为了减少代码的书写
//带有默认参数的函数重载容易产生二义性,谨慎使用!
#if 0
int add(int x)
{
    return x + 1;
}

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

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

int main(int argc, char* argv[])
{
    int a = 3, b = 4, c = 5;
    cout << "add(a) = " << add(a) << endl;
    cout << "add(a, b) = " << add(a, b) << endl;
    cout << "add(a, b, c) = " << add(a, b, c) << endl;
    return 0;
}

9. bool类型

#include <iostream>

using std::cout;
using std::endl;

void test()
{
    int u = sizeof(bool);
    int v = sizeof(int);
    int x = true;
    int y = false;
    int z = sizeof(x);
    cout << "u = " << u << endl;
    cout << "v = " << v << endl;  
    cout << "x = " << x << endl;
    cout << "y = " << y << endl;
    cout << "z = " << z << endl;
   
    cout << "=======" << endl;
    //在C++中,添加了一种基本类型,bool类型,表示true和false
    //bool占用一个字节大小空间,可以作为标志位使用
    bool a = -10;
    bool b = 0;
    bool c = 1;
    bool d = 10;
    bool e = true;
    bool f = false;
    
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;  
    cout << "c = " << c << endl;
    cout << "d = " << d << endl;
    cout << "e = " << e << endl;
    cout << "f = " << f << endl;
}

int main(int argc, char* argv[])
{
    test();
    return 0;
}

运行结果:
在这里插入图片描述

10. 内联(inline)函数

10.1 什么是内联函数

内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。定义函数时,在函数的最前面以关键字“inline”声明函数,即可使函数称为内联声明函数。

#include <iostream>

using std::cout;
using std::endl;
//带参数的宏定义,发生的时机在预处理阶段
//简单文本替换,效率高于一般函数调用(存在较大的切换开销)
//出现bug,要到运行时候才可以发现
#define multiply(x,y) ((x) * (y))

/*普通函数的定义
 *在大多数的机器上,调用函数都要做很多工作:
调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必
须转向一个新位置执行。即对于这种简短的语句使用函数开销太大。
int add(int x,int y)
{
    return x + y;
}
*/

//内联声明函数,使用关键字inline
//函数调用时,可以用函数体替代
//兼具宏定义的“替换”,又能在编译阶段进行优化分析
//内联函数一般不建议写入for/while,也不建议函数的嵌套使用
//好的现代编译器,可以“赋予”内联(短小而简单的函数),或“取消”不必要的内联
inline 
int add(int x, int y)
{
    return x + y;
}

int main(int argc, char* argv[])
{
    int a = 3, b = 4, c = 5, d = 6;

    cout << "add(a,b) = " << add(a,b) << endl;
    cout << "multiply(a + b, c + d) = "
         << multiply(a + b, c + d) << endl;

    return 0;
}

10.2 头文件和实现文件

错误示例:

//add.h头文件
#ifndef __ADD_H_
#define __ADD_H_
inline
int add(int x, int y);
/* #include “add.cc" */
#endif
//add.cc实现文件
inline
int add(int x, int y)
{
    return x + y;
}
//testAdd.cc测试文件
#include "add.h"
#include <iostream>

using std::cout;
using std::endl;

int main(int argc, char* argv[])
{
    int a = 3, b = 4;
    cout << "add(a,b) = " << add(a,b) << endl;
    return 0;
}

运行结果分析:
在这里插入图片描述

11. 异常安全

异常是程序在执行期间产生的问题。C++异常是指在程序运行时发生的特殊情况,异常提供一种转移程序控制权的方式。C++异常处理涉及三个关键字:

  • try:try块中的代码标志将被激活的特定异常,它后面通常有一个或多个catch块;
  • throw:当问题出现时,程序会抛出一个异常;
  • catch:在想要处理问题的地方,通过异常处理程序捕获异常。
/* try - throw -catch语句块 */

try	   //尝试捕获
{
		throw ...  //抛出
}
catch //捕获
....
catch //捕获

try…catch语句块的catch可以有多个,但至少要有一个。
try…catch语句的执行过程是:

  • 执行 try块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch块后面的语句,所有 catch块中的语句都不会被执行;
  • 如果 try块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch块中执行(称作异常被该 catch块“捕获”),执行完后再跳转到最后一个 catch块后面继续执行。
    举例如下:
#include <iostream>

using std::cout;
using std::endl;
using std::cin;

void test()
{
    double x, y;
    cin >> x >> y;

    try
    {
        if(0 == y)
        {
            throw y;
        }
        else
        {
            cout << "(x/y) = " << x/y << endl;
        }
    }
    catch(int c)
    {
        cout << "catch(int)" << endl;
    }
    catch(double d)
    {
        cout << "catch(double)" << endl;
    }

}

int main(int argc, char* argv[])
{
    test();
    return 0;
}

运行结果:
在这里插入图片描述

12. C/C++风格字符串

#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

void test1()
{
    //C定义字符串是以'\0'结尾,两种形式
    //1、字符数组
    // *const
    char str1[] = "hi";
    char str2[] = "world";
    str1[0] = 'H';
    printf("str1 = %s\n", str1);
    printf("&str1 = %p\n", str1);
    /* str1 = 0x12344566;error */
   
    cout << "=========" << endl;
   
    //2、字符指针
    //const*
    //字符指针最好写:const char* pstr = "hi,world";
    char* pstr = "hi,world";
    printf("pstr = %s\n", pstr);
    printf("&pstr = %p\n", pstr);
    pstr = "hello";
    printf("pstr = %s\n", pstr);
    printf("&pstr = %p\n", pstr);

    /* pstr[0] = 'H'; error */
    cout << "=========" << endl;
   
    //字符串如何获取长度?
    size_t len1 = sizeof(str1);
    size_t len2 = sizeof(str2);
    printf("len1 = %ld\n", len1);
    printf("len2 = %ld\n", len2);
    
    size_t length1 = strlen(str1);
    size_t length2 = strlen(str2);
    printf("length1 = %ld\n", length1);
    printf("length2 = %ld\n", length2);
    
    cout << "=========" << endl;
    //测试指针大小的时候,不要使用sizeof,与系统相关
    size_t pstr_len = sizeof(pstr);
    size_t pstr_length = strlen(pstr);
    printf("pstr_len = %ld\n",pstr_len);
    printf("pstr_length = %ld\n",pstr_length);
    
    //如何将str1与str2拼接到一起?
    size_t len3 = len1 + len2 - 1;
    /* char str[len3];//栈区 */
    char* pstr2 = static_cast<char*>(malloc(len3));
    memset(pstr2, 0, len3);
    strcpy(pstr2, str1);
    strcat(pstr2, str2);
    printf("pstr2 = %s\n", pstr2);

    free(pstr2);
    pstr2 = nullptr;
}

void test2()
{
    //C++风格 <====  C风格
    string s1 = "hi";
    string s2 = "world";
    string s3 = s1 + s2;//不需要考虑与内存相关的操作
    cout << "s1 = " << s1 << endl
         << "s2 = " << s2 << endl
         << "s3 = " << s3 << endl;

    cout << "=========" << endl;
    //C风格  <====  C++风格
    const char *pstr = s3.c_str();
    cout << "pstr = " << pstr << endl;

    //获取C++风格字符串的长度
    size_t len1 = s3.length();
    size_t len2 = s3.size();
    cout << "len1 = " << len1 << endl;
    cout << "len2 = " << len2 << endl;
    
    //遍历C++风格字符串
    for(size_t idx = 0; idx != s3.size(); ++idx)
    {
        cout << s3[idx] << "  ";
    }
    cout << endl;

    //C++风格字符串的拼接
    string s4 = s3 + "come";
    cout << "s4 = " << s4 << endl;
    
    string s5 = s4 + 'A';
    cout << "s5 = " << s5 << endl;

    s5.append(s1);//在s5后面拼接s1
    cout << "s5 = " << s5 << endl;

}
int main(int argc, char* argv[])
{
    test1();

    cout << "=========" << endl;

    test2();
    return 0;
}

运行结果如下:
在这里插入图片描述
相关库函数请查阅cppreference:link

13. 程序内存布局

现在的应用程序都运行在一个虚拟内存空间里,以32位系统为例,2^32=4G,其寻址空间为4G,大部分的操作系统都将4G内存空间的一部分挪给内核调用,应用程序无法直接访问这一段内存,这一部分内核地址成为内核态空间,Linux默认将高地址的1G空间(3G-4G)分配给内核,用户使用剩下的**3G空间(0-3G)**成为用户态空间,用户态空间一般有如下默认区域:

  • 栈区:栈变量、栈对象。局部变量、函数的参数。编译器进行自动分配与释放。
  • 堆区:malloc/calloc/new申请的空间都是堆空间。需要程序员进行申请与释放,如果程序员不回收,那么os可能会进行回收。
  • 全局静态区(读写区)
    • 全局变量:初始化与未初始化的。
    • 静态变量:初始化与未初始化的,static修饰的。
  • 只读区
    • 文字常量区:可以存放字符串常量。“Helloworld".
    • 程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码。
      虚拟内存空间示意图如下:
      在这里插入图片描述
      测试代码如下:
#include <iostream>

using std::cout;
using std::endl;

int a;//全局变量,全局静态区,默认初始化为0
char* p1;//全局变量,本身在全局静态区,默认初始化为空
const int KMax = 100;//文字常量区
int main(int argc, char* argv[])
{
    const int KMin = 1;//栈区,在栈上的常量
    int b;//局部变量,栈区,默认初始值为随机
    char* p2;//局部变量,栈区,默认初始值为随机
    static int c = 10;//静态变量,全局静态区
    int* pInt = new int(10);//pInt本身位于栈区,但所指变量位于堆区
    delete pInt;
    pInt = nullptr;
    const char* pstr = "helloworld";//pstr本身位于栈区,但所指区域为文字常量区
    printf("\n打印变量的地址\n");
    printf("&a = %p\n", &a);
    printf("&p1 = %p\n", &p1);
    printf("p1 = %p\n", p1);
    printf("&b = %p\n", &b);
    printf("&p2 = %p\n", &p2);
    printf("p2 = %p\n", p2);
    printf("&c = %p\n", &c);
    printf("&pInt = %p\n", &pInt);
    printf("pInt = %p\n", pInt);
    printf("&pstr = %p\n", &pstr);
    printf("pstr = %p\n", pstr);
    printf("&\"helloworld\" = %p\n", &"helloworld");
    printf("&main = %p\n", &main);
    printf("main = %p\n", main);
    printf("&KMax = %p\n", &KMax);
    printf("&KMin = %p\n", &KMin);
    printf("\n打印变量的值\n");
    printf("a = %d\n", a);
    printf("b = %d\n", b);

    return 0;
}
  • 栈与堆的比较
    • 1.申请后系统的响应
      栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
      堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,首地址处会记录这块内存空间中本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
    • 2.申请效率
      栈由系统自动分配,速度较。但程序员无法控制。
      堆是由new分配的内存,一般速度比较,而且容易产生内存碎片,不过用起来最方便。
    • 3.申请大小的限制
      栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
      堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间比堆的大小受限于计算机系统中有效的虚拟内存较灵活,也比较大。
    • 4.堆和栈中的存储内容
      栈: 在函数调用时,第一个进栈的是主函数的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
      堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
    • 5.总结五个方面
    1. 管理方式不同。对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak;
    2. 空间大小不同。一般来讲在32位系统下,内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VS下,默认的栈空间大小是1M;
    3. 分配方式。内存有2种分配方式:静态分配和动态分配。堆都是动态分配的,没有静态分配的堆。静态分配是编译器完成的,比如局部变量的分配。动态分配由malloc, calloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
    4. 生长方向。对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
    5. 碎片问题。对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在它上面的后进的栈内容已经被弹出。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值