各行各业网站建设,渠道销售怎么找客户,大连市公众平台网站,凡科网站建设套餐报价Linux下编译C程序
安装g命令#xff1a;sudo apt install g
编译命令#xff1a;$ g *.cc 或者 *.cpp -o fileName;
hellworld 编译程序可以看到#xff1a;
namespace命名空间
首先#xff0c;命名空间的提出是为了防止变量重名冲突而设置的。 浅浅试一下#xff1…Linux下编译C程序
安装g命令sudo apt install g
编译命令$ g *.cc 或者 *.cpp -o fileName;
hellworld 编译程序可以看到
namespace命名空间
首先命名空间的提出是为了防止变量重名冲突而设置的。 浅浅试一下 现在我们进行编译的时候会发现报错 从报错提示可以看到他希望我们使用wd::display()的方式来调用该函数。::称为作用域限定符 这是因为display函数是定义在namespace命名空间里的所以想要使用其内置成员的时候我们需要加上其对应的空间名 再来编译 完美运行通过。
命名空间还可以嵌套使用 完美运行 这是命名空间的一种使用方式还有一种则是如下 使用了using编译指令之后就可以不用再带对应的命名空间名了因为上图中using编译指令会将std该空间的所有实体全部引入。
注意第二种方式使用时必须知道该空间中有哪些实体如果不知道这样的写法就依然存在可能造成冲突的风险。
什么意思 我们来试一下这里使用std标准命名空间测试 可以看见我们的cout函数具有二义性因为std中也有一个该函数编译会报错 ambiguous二义性。
所以初学时大型项目里面最好不要使用using编译指令因为有可能造成冲突你并不知道std中有多少函数。
推荐使用using声明机制即using std::cout; 它只引入这一个实体。 另外在命名空间中直接定义的实体不要直接缩进。
匿名命名空间
其实就是不带空间名就是匿名的命名空间匿名命名空间可以直接使用其内部定义的实体。
#include iostream
using namespace std;
//匿名命名空间
namespace {int number 4;}int main(void){cout number endl;return 0;
}
我们看存在的一种情况
#include iostream
using namespace std;
//匿名命名空间
namespace {int number 4;}int number 5;int main(void){cout number endl;return 0;
}
此时不出意料肯定会报错因为具有二义性 所以我们为了强调我们用的是哪个number就需要使用匿名空间的作用域操作符
#include iostream
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 iostream
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 iostream
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_numbercout g_number endl;return 0;
}
编译运行
另外在同一个模块中可以定义多次命名空间在不同的模块中也可以定义多次命名空间
#include iostreamusing namespace std;//第一次定义命名空间wd
namespace wd{void show(); //这里是第一次声明实体show()
}int main(){//调用wd中的show()wd::show();return 0;
}//这里我们第二次定义命名空间wd
namespace wd{//第二次定义实体showvoid show();
}//这里我们第三次定义命名空间wd
namespace wd{//第三次声明并且定义实体showvoid show(){cout 我是第三次被声明啦 endl;}
}
注意虽然命名空间可以随便声明但是它里面的函数声明可是只能有一次定义嗷就和正常的函数一样。 编译运行 不光是本文件可以重复声明命名空间跨文件或者说跨模块也一样可以这里我们在namespace3文件中定义一个同名wd
#includeiostream
using namespace std;//在namespace3.cpp文件中定义重名namespace wd
namespace wd
{void print(){cout 我是跨模块的命名空间嗷 endl;}
}
然后我们返回到刚刚的测试文件中去调用它 #include iostreamusing namespace std;//在这里调用跨文件的namespace wd
namespace wd{void print();
}//第一次定义命名空间wd
namespace wd{void show(); //这里是第一次声明实体show()
}int main(){//调用wd中的show()//wd::show();//调用跨文件的namespace3.cpp中的wdwd::print();return 0;
}//这里我们第二次定义命名空间wd
namespace wd{//第二次定义实体showvoid show();
}//这里我们第三次定义命名空间wd
namespace wd{//第三次声明并且定义实体showvoid show(){cout 我是第三次被声明啦 endl;}
}
编译运行
总结 1、命名空间的提出是为了防止变量重名冲突而设置的可以嵌套使用 2、去除了命名空间名就是所谓的匿名命名空间匿名命名空间的实体无法跨模块调用。 3、在同一个模块中可以定义多次命名空间在不同的模块中也可以定义多次命名空间。
const修饰类型和对象
const修饰类型或对象成为常量值的关键字常量值不可以改变且必须初始化。
#include iostreamusing namespace std;#define MAX 1000void test(){int a;//const int b; 必须要继续初始化否则报错const int c 1;//c 2; error 常量是不能进行修改的//有同样效果的还有宏定义#definecout MAX endl;
}int main(){test();return 0;}
宏定义与const常量的区别面试常考 1、发生的时机不一样 宏定义是在预处理时而const常量是在编译时 2、类型检查不一样 宏定义是没有类型检查的只是简单做了字符串的替换虽然也有编译阶段但在编译阶段没有报错将出错的时机推迟到了运行时但运行时阶段的错误是更难发现的而const是由类型检查的这样更加安全一些 那么什么叫宏定义只是作了字符串的简单替换呢 这里我们举例说明
#include iostreamusing namespace std;//举例说明为什么宏定义只是进行了简单的字符串替换
#define kBase 34void 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文件就是我们的预处理文件打开它可以看见
34被当作字符串一样直接替换了kBase所以最后的结果就成了10*34-34。
总结要定义常量时最好使用const或者枚举enum类型。
const修饰指针
# include iostreamusing 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 iostreamusing namespace std;int main(){//new表达式执行完毕之后返回的是相应类型的指针int* pint new int(1);cout *pint *pint endl;
}
运行编译 但此时我们的代码是有问题的因为没有释放掉我们的pint空间即发生了内存泄漏。 那我们怎么检测我们的程序是否发生了内存泄露呢 答案是使用一些内存检测工具比较常用的如valgrind 执行下面的命令下载它
内存泄露检测工具-valgrind面试高频考点
下载完毕后我们执行以下操作
上面的操作结束后现在我们就可以直接使用别名命令memcheck来检测内存泄露了我们来检测一下我们刚刚的内存是否存在泄露 从in use at exit4bytes in 1 blocks等信息中可以看出存在内存泄露问题着就是内存检测工具valgrind的简单使用。
所以要记得回收内存啊
关于new还有一种使用方式
#include iostreamusing 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;}
引用 引用作为函数参数传递
首先回忆一下之前的几种参数传递方式
1、值传递
还是使用经典的交换两个变量值的内容来作例子 由上图可知在使用值传递时其实传递的仅仅是变量值的拷贝而我们建立的在swap函数中定义的tmp值也不过是个临时变量当swap函数执行结束后tmp变量也就随即消失了。所以值传递并不能实现交换两个值的内容这是由于两个函数并不共享一块内存空间决定的虽然它们都存在在栈空间内。 注意图中的箭头仅仅意味着进行了一个参数的拷贝而已即a1的值拷贝给了变量x。
2、地址传递 地址传递就不一样了上图明显可以看见指针px指向了a的地址指针py指向了b的地址。 所以对两个指针解引用可以得到*px 1; *py 2 在swap函数中第20行代码tmp暂存了 *px的值然后第二十一行中px所指地址空间上的值被修改成了 *py的值即 *px 2 第22行代码则将 *py的值变成了 1。 所以达到了我们想要交换两个变量值的目的
3、引用传递 这就没啥好说的了因为引用其实就是别名所以操作x和y其实就是在直接操作a和b罢了。
引用的出现就是为了替代指针尽量让程序员减少犯错的概率。 其底层实现依然是指针而且是一个受限制的指针即引用一旦被绑定到某个对象上后就不能够解绑去绑定到别的对象上。
引用作为函数的返回值 强制转换与函数重载
C风格强制转换
TYPE a (TYPE) EXPRESSION;但这种风格存在缺陷就是安全性不足无法保证转换之后类型的合法性。
而C风格的强制转换就不一样了
有四种
1、static_cast最常用比如常见的指针转换把void*转换成其它类型的指针2、const_cast去除常量属性3、dynamic_cast动态类型转换只用在多态时基类与派生类之间的转换4、reinterpret_cast在任意类型之间轻易转换但是不要轻易使用用的最少上面四种转换方式只是含义不一样但写法是通用的形式都如下
static_cast
这个没什么好讲的就是正常用就型
int* p static_castint* (malloc(sizeof(int)));上面这一句代码就是一个很典型的应用在C风格中malloc函数返回的是void*如果要使该行代码不报错的话就必须进行类型转换那么此时用static_cast是非常合适的。
const_cast #include iostreamusing namespace std;void display(int* p){ //明显要求传入一个非const指针*p 10;cout *p *p endl;}int main(){const int a 1;display(a);//在实参传递时只有const变量如果传递成功的话就有修改a的值的风险}
如上面注释所说这肯定是报错的 那如果我们一定要传这个const常量参数呢 那就可以用上const_cast了
#include iostreamusing namespace std;void display(int* p){ //明显要求传入一个非const指针*p 10;cout *p *p endl;}int main(){const int a 1;//进行了去除const的强制类型转换display(const_castint* (a));}
现在再运行就没有问题了 虽然我们使用了const_cast进行转换但是我们并没有真正改变const变量的值这一点要注意即上面代码中的a的值是没有变化的。
而且更有意思的是当我们打印指针p的值即变量a的地址的时候会发现它的地址居然和常量a是一模一样的
#include iostreamusing 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_castint* (a));cout a的地址为: a endl;}
运行结果 这就很扯地址值是一样的但是值不一样。
所以迷惑性很强一般情况下最好不要用const_cast。这里很多资料里面都没有一个明确的说法据说是*p的值存在了所谓的寄存器中并没有真正写入内存啥的反正知道有这么回事就行了
dynamic_cast和reinterpret_cast两个就不讲了基本用不到
函数重载 C语言不支持函数重载 由上图可以发现确实对于不同的重载函数其实就是改变一下对应的名字来调用而已。add是函数名然后add后面的ii就是参数列表中各个参数的缩写。
C与C的混合编程
上一节我们知道了C在内部是使用了名字改编的原理来支持函数重载的但是C语言不支持函数重载自然也就没有所谓名字改编的操作这就导致了C和C在进行混合编程的时候会出现一些兼容问题 为什么需要进行混合编程很明显C比C早十二年出来很多库都是C写的C只能去兼容和适应C的法则 为了解决这样的问题我们引入了下面的方式来解决兼容性问题 上图右侧就是混合编程的编译结果示例代码如下
#include iostreamusing 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中的条件编译宏定义 所以在头文件中加上上述内容
//宏_cplusplus只有C的编译器才会定义
//C的编译器没有该宏
//意思就是只有该被包围起来的代码是被C编译器编译时才会出现
//若是被C编译器编译的话就不会出现
#ifdef _cplusplus
extern C
{
#endifint add(int x,int y){return x y;}
#ifdef _cplusplus
}
#endif
通过上述方法就可以完美解决C与C的混合编程问题。
默认参数 这其实没啥好说的就注意一下上面说的一个点 默认参数的设置要求必须从右到左进行 另外设置默认参数的时候要注意是否有其它的重载函数与其设置了默认参数的参数列表产生调用时的二义性就行。
inline函数
首先在C语言中其实有类似的语法函数宏定义
#include iostreamusing namespace std;//C语言中的函数宏定义
#define multiply(x,y) x * yint main(){int a 3, b 4;int c 5, d 6;cout multiply(a,b) endl;//输出为12//但还是之前的问题宏定义只是简单替换成了字符串//所以下面的语句其实是ab*cd 29cout multiply(ab,cd) endl;
}
编译运行 接下来我们看在C中有同样功能的inline函数
#include iostreamusing 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//但还是之前的问题宏定义只是简单替换成了字符串//所以下面的语句其实是ab*cd 29cout multiply(ab,cd) endl;//调用inline内联函数cout divide(d,a) endl;
}
为了降低犯错误的概率尽量使用inline内联函数。
内联函数的使用要求 C内存布局面试常考 上图是每一个进程被装载到内存中运行时的内存空间分布示意图每个进程被装载到内存中运行时都会有如上几个区。 32位操作系统意思就是每一次读写数据的话只能读写32个位也就是四个字节因为只有32根地址线来传送数据所以32根地址线最大传送的数据就是当这32个位全为1的时候最小就是当这三十二个位全为0的时候这就决定了该类型操作系统的内存地址空间范围那么2的32次方也就是4G大小的内存空间其中一部分用来作OS的系统空间即上图中的内核态用来运行一些内核程序而剩下的部分就是用户去区也就是上图的用户态用户进程也就是我们所编写的程序都会运行在用户态中。我们的C程序也一样会运行在用户态里只不过完整的程序根据其代码的不同会被分到不同的内存区域中其中栈区总是位于虚拟地址的高位部分向低地址方向进行生长而堆区则在其下面由低地址向高地址生长全局/静态区或者说读写段和只读段或者说文字常量区和程序代码区则依次往下存放。
接下来我们来一一验证通过本次学习以后必须清楚自己写下的每一句代码中的数据是存储在哪个空间里的。
#include iostreamusing 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的意思则是取整个字符数组的地址也就是首个元素的地址* 但此时对pstr21的话我们不会拿到第二个元素的地址反而是会把整个字符数组当作第一个元素然后去访问下一个字符数组* 的元素也就是偏移的是整个数组的长度而不是偏移一个元素的长度*/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);}
编译运行
自己可以对照着看看加深一下理解然后下面是对上面代码中提到的pstr2和*pstr2的区别图示