C/C++程序设计的基本概念详解

目录
  • 概述
  • main()函数
  • 内部名称
  • 变量和它的初始化
  • 编译时和运行时
  • 总结

概述

学C语言有很长一段时间了,想做做笔记,把C和C++相关的比较容易忽视的地方记下来,也希望可以给需要的同学一些帮助。

我的这些文章不想对C和C++的语法进行讲解和罗列,这些东西随便找一本书就讲的比我清楚,我只是想把一般人忽视的地方尽自己所能描述一下。权当班门弄斧,贻笑大方了。

首先我想先从C和C++的一些基本概念入手。

main()函数

稍微学过C和C++的人都知道main()函数市所有C和C++程序必不可少的东西。叫做主函数。所有的程序都应该从main()函数开始执行。但是你们又对这个函数了解多少呢?

我们都知道C和C++是一种函数语言,几乎绝大多数的功能都是通过各种函数的调用来实现的,C和C++也提供了丰富的函数库供编程人员调用。可虽然main()函数每个C程序都必须有的函数,在C或者C++的函数库里却没有叫做main()的函数,它是需要程序设计人员实现的函数。

而且,你们发现了没有,main并不是C和C++的保留字。因此理论上,你可以在其他地方使用main这个名字,比如变量名、类名字、名字空间的名字甚至成员函数的名字。但是,即使这样,你也不能修改main()函数本身的函数名,否则连接器就会报告错误。

main()函数是C和C++程序的入口,这是因为C和C++语言实现会有一个启动函数,比如MS-C++的启动函数就叫做

mainCRTStartup()或者WinMainCRT-Startup()。在这个启动函数的最后会调用main()函数,然后再调用exit()函数结束程序。如果没有main()函数,当然会报错了。所以再C和C++开发环境中main()函数其实是一个回调函数。它是需要我们来实现的。

有些同学可能学过一些应用程序框架,比如MFC什么的。这些程序代码中往往找不到main()函数,这是因为那些应用程序框架把main()函数的实现给隐藏起来了,main()函数在它们这里有固定的实现模式,所以不需要我们编写。在连接阶段,框架会自动将包含main()实现的库加进来连接。

main()函数也是有原型的。这个原型已经是一种标准了,在ISO/IEC14882中对main()的原型进行了定义。

	int main(){/*......*/}
和
	int main(int argc, char *argv[]){/*......*/}

上面这两种形式是最具有可移植性的正确写法。当然不同的编译器可能会允许出现一些扩展。比如允许main()返回void,或者有第三个参数char *env[]什么的。这个就要看具体的编译器文档了。

关于返回值,我们知道main()返回的是int类型的。到底返回什么是有不同含义的。一般情况下,返回0,表示程序正常结束,返回任何非0表示错误或非正常退出。前面讲到了,启动函数最后还会调用exit()函数。那么main()函数的返回值就会作为exit()函数的操作数来返回操作系统。

在C++当中对main()函数还有一些特殊的限制。比如:

  • 不能重载
  • 不能内联
  • 不能定义为静态的
  • 不能取其地址
  • 不能由用户自己调用

关于main()函数的参数,它可以让编译好的执行程序具有处理命令行参数的能力。这里需要注意,不要把“命令行参数”和main()函数的“函数实参”混淆,这是两个不同的概念。命令行参数由启动程序截获并打包成字符串数组传递给main()的形参argv[],而包括命令字(也就是执行文件自己的名字)在内的所有参数的个数则被传递给形参argc。试一下吧,咱们来模拟copy命令写个简单的文件拷贝程序。

//mycopy.c:文件拷贝程序。
#include <stdio.h>
int main(int argCount, char* argValue[])
{
    FILE *srcFile = 0;
    FILE *destFile = 0;
    int ch = 0;
    if(argCount != 3)
    {
        printf("使用方法:%s 原文件 目标文件\n",argValue[0]);
    }
    else
    {
        if((srcFile = fopen(argValue[1],"r")) == 0)
        {
            printf("无法打开原文件!\"%s\"!",argValue[1]);
        }
        else
        {
            if((destFile = fopen(argValue[2],"w")) == 0)
            {
                printf("无法打开目标文件!\"%s\"!",argValue[2]);
                fclose(srcFile);
            }
            else
            {
                while((ch = fgetc(srcFile)) != EOF)
                {
                    fputc(ch,destFile);
                }
                fclose(srcFile);
                fclose(destFile);
                return 0;
            }
        }
    }
    return 1;
}
//用法:mycopy C:\file1.dat D:\newfile.dat

内部名称

在编写C程序的时候如果没有main()函数,连接器会报错。一般报错信息会提示“unresolved external symbol_main”。这里的"_main"其实就是编译器为main生成的内部名称。其实C和C++语言在编译过程中都会按照特定的规则把用户定义的标识符(函数、变量、类型、名字空间什么的)转换为相应的内部名称。而这些规则还跟指定的连接规范有关。比如,在C语言中,main的内部名称就叫做_main。

C语言这么做,是告诉连接器,这个东西是个函数。实际上,C语言在所有函数的函数名前其实都是加了前缀“_”的,以此来区别函数名和其他标识符名称。

这种规范在C++又是另一种样子。这是因为在C中,所有函数只要不是局部于编译单元(文件作用域)的static函数,就会是具有extern连接类型的和global作用域的全局函数。全局函数是不可以有同名的。但是在C++里面,可以在不同的作用域,比如class,struct,union,namespace中定义同名的函数,甚至在同一个作用域也可以定义同名函数,也就是函数重载。那么转换为内部名称的连接规范就要复杂一些了。比如:

class Sample_1
{
	char m_name[16];
public:
	void foo(char *newName);
	void foo(int age);
};
class Sample_2
{
	char m_name[16];
public:
	void foo(char *newName);
	void foo(bool sex);
};

在其他地方根据这两个类生成两个实例,并进行操作:

	Sample_1 a;
	Sample_2 b;
	a.foo("aaa");
	a.foo(100);
	b.foo("bbb");
	b.foo(false);

这里有四个函数,但是确是同一个名称。编译器应该怎么区分呢?通过各自对象的成员标识符区分?那是在代码中区分的,但是在连接器看来,所有函数其实都是全局函数,而全局函数是不能重名的。所以为了避免二义,在C++中有一个名字修饰规则。也就是在函数名前面添加各级作用域的名称以及重载函数经过编码的参数信息。比如上面四次调用foo函数,其实它们会调用四个具有全局名称的函数,分别是Sample_1_foo@pch@1,Sample_1_foo@int@1,Sample_2_foo@pch@1,Sample_2_foo@int@1。

然而,这种标准并不是强制的,所以不同厂家开发的C++编译器有可能会有些许不同,而这也正是导致不同厂家的C++编译器和连接器不能兼容的原因。

那么好了,当使用不同编程语言联合开发时候,就要定义一个统一的规范,这个规范叫做连接规范。这个很好理解了吧,因为如果同一个标识符在不同编译单元中用不同的连接规范,就会产生不一致的内部名称,连接肯定会失败。

所以,在开发程序库的时候就一定要明确你要用那条连接规范。比如,编写C程序是就要规定C连接规范:extern “C”。大约有这么几种情况:

  • 仅对一个类型、函数、变量或常量指定连接规范;
    extern "C" void WinMainCRTStartup();
    extern "C" const CLSID CLSID_DataConverter;
    extern "C" struct Student{/*....*/};
    extern "C" Student g_Student;
  • 对一段代码限定连接规范
    #ifdef __cplusplus
    extern "C" {
    #endif
    const int MAX_AGE = 200;
    #pragma pack(push,4)
    typedef struct _Person
    {
        char *m_Name;
        int m_Age;
    } Person, *PersonPtr;
    #pragma pack(pop)
    Person g_Me;
    int __cdecl memcmp(const void*, const void*, size_t);
    void* __cdecl memcpy(void*, const void*, size_t);
    void* __cdecl memset(void*, int, size_t);
    #ifdef __cplusplus
    }
    #endif
  • 当前使用的是C++编译器,并且使用了extern "C"限定了一段代码的连接规范,但又想在其中某行或某段代码保持C++的连接规范。
    #ifdef __cplusplus
    extern "C" {
    #endif
    const int MAX_AGE = 200;
    #pragma pack(push,4)
    typedef struct _Person
    {
        char *m_Name;
        int m_Age;
    } Person, *PersonPtr;
    #pragma pack(pop)
    Person g_Me;
    #if __SUPPORT_EXTERN_CPP_
    extern "C++"{
    #endif
    int __cdecl memcmp(const void*, const void*, size_t);
    void* __cdecl memcpy(void*, const void*, size_t);
    #if __SUPPORT_EXTERN_CPP_
    }
    #endif
    void* __cdecl memset(void*, int, size_t);
    #ifdef __cplusplus
    }
    #endif
  • 某个声明中指定了某个标识符的连接规范为extern “C”,那么对应的定义也要指定extern “C”:
    #ifdef __cplusplus
    extern "C" {
    #endif
    memcmp(const void*, const void*, size_t);
    #ifdef __cplusplus
    }
    #endif
    #ifdef __cplusplus
    extern "C" {
    #endif
    memcmp(const void *p, const void *a, size_t len)
    {
        //功能实现
    }
    #ifdef __cplusplus
    }
    #endif

其实如果是面向接口的编程,就不用考虑这么多了。因为即使接口两端的内部名称不同,只要使用了一致的成员对其和排列方式,并遵守一致的调用规范,一致的函数实现方式。也就是C++一致的对象模型,那么基本不会有什么问题的。

变量和它的初始化

在C和C++中,全局变量(extern或static的)存放在程序的静态数据区里面。这些变量在程序进入main()之前就被创建了,并在main()结束后销毁,C和C++提供了一个默认的全局初始化器0。也就是编译器会默认的用0来初始化它们。函数内部的static变量和类的static成员也是在静态存储区,因此也会默认初始化为0。除非你在创建的时候就提供了初值。这是编译器对静态变量的待遇。而对于其他的自动变量,就需要我们给他初始化了。不要指望编译器自动对它们初始化。

所以,全局变量的声明和定义应当放在源文件的开头部位。

变量的初始化和变量的赋值是有区别的。初始化是发生在变量创建的同时,而赋值是在程序中变量创建后干的。
前面说了,对静态存储区的变量(比如全局变量,静态变量什么)的进行初始化是编译器自动进行的,但是局部变量的初始化确实需要编程人员手动进行。

还有,在一个编程单元中,全局变量的初始值不要依赖另一个编译单元的全局变量。什么意思?比如:

//file1.c
int g_x = 100;
//file2.c
extern int g_x;
double g_d = g_x + 10;

这两个编译单元编译完成后进行连接,两个全局变量到底先初始化哪个并不确定,连接器也不能保证这一点。先初始化g_x,那g_d也就能顺利初始化,而反之,g_d就不一定是多少了。

另外,C和C++都会有现成的库。就是文件开头包含的那些*.h文件。注意哦,C的库可是有多线程版和单线程版,开发多线程程序应该使用多线程版本的库。另外,在多人开发软件是,库的版本一定要统一。

编译时和运行时

源代码文件编写的功能有些时运行时起作用,有些编译时就起作用的。这件事需要区分的。比如预编译伪指令、类定义、外部对象声明、函数原型、修饰符号(const,static那些)、类成员访问说明符号(public、private那些)以及连接规范是在编译阶段发挥作用的,可执行程序里是不存在这些东西的。而容器越界访问、虚函数动态决议、动态连接、动态内存分配、异常处理、RTTI这些则是在运行时发挥作用的。比如:

int* pInt = new int[10];
pInt += 100;
cout << *pInt << endl;
*pInt = 1000;

这段代码一般在编译阶段没什么问题,但运行时会出错。所以,我们在程序设计时就要对运行的行为有所预见,通过编译连接的程序在运行时不见得正确。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • 通过C++程序示例理解设计模式中的外观模式

    举一个生活中的小例子,大凡开过学或者毕过业的都会体会到这样一种郁闷:你要去 n个地方办理 n 个手续(现在大学合并后就更加麻烦,因为可能那 n 个地方都隔的比较远). 但是实际上我们需要的就是一个最后一道手续的证明而已,对于前面的手续是怎么办的.到什么地方去办理我们都不感兴趣. 实际上在软件系统开发中也经常回会遇到这样的情况,可能你实现了一些接口(模块),而这些接口(模块)都分布在几个类中(比如 A 和 B.C.D):A 中实现了一些接口,B 中实现一些接口(或者 A 代表一个独立模块,B.C.

  • C++ 面向对象程序设计--内存分区详解

    目录 一.分区的意义 二.代码区 1.定义 2.特点 三.全局区 1.定义 2.特点 3.相关代码 1)全局变量 2)静态变量 四.栈区--程序运行后 1.定义 2.相关代码 五.堆区--运行后 1.定义 2.相关代码和运行结果 总结 一.分区的意义 在讲分区前,先谈谈内存分区的意义,也就是为什么程序要进行分区? 笔者认为这是为了编程的灵活性,因为将内存分区后,不同区域的内存,相关的数据就有的不同的生命周期.以笔者之前的一篇算法复杂度的blog中提到栈帧空间为例,在此就是指栈区,而栈区多指非ma

  • Visual C++程序设计中Windows GDI贴图闪烁的解决方法

    本文实例讲述了Visual C++程序设计中Windows GDI贴图闪烁的解决方法.分享给大家供大家参考.具体如下: 一般的windows 复杂的界面需要使用多层窗口而且要用贴图来美化,所以不可避免在窗口移动或者改变大小的时候出现闪烁. 先来谈谈闪烁产生的原因 原因一: 如果熟悉显卡原理的话,调用GDI函数向屏幕输出的时候并不是立刻就显示在屏幕 上只是写到了显存里,而显卡每隔一段时间把显存的内容输出到屏幕上,这就是刷新周期. 一般显卡的刷新周期是 1/80秒左右,具体数字可以自己设置的. 这样

  • C++程序设计-五子棋

    前言:很多刚刚接触编程的人都不知道怎么下手编写程序,特别是学习了新的知识点,不知道有什么用,那么本文将以简单的存储结构及简单的运算,条件语句,分支语句,循环语句结合,带来一个双人对战版五子棋,这是一个简单的模型,实现了五子棋最最基本的功能,还有好多地方需要补全,如边界问题,设计问题,游戏逻辑问题,希望读者阅读后能够注意,通过自己的努力来完善它,还能扩展各种功能,如悔棋,网络对战等,有时候写程序和小生命一样,慢慢会成长,而我们作为"父母"的看到自己的小宝宝成为有用之才,过程之欣喜特别棒!

  • C/C++程序设计的基本概念详解

    目录 概述 main()函数 内部名称 变量和它的初始化 编译时和运行时 总结 概述 学C语言有很长一段时间了,想做做笔记,把C和C++相关的比较容易忽视的地方记下来,也希望可以给需要的同学一些帮助. 我的这些文章不想对C和C++的语法进行讲解和罗列,这些东西随便找一本书就讲的比我清楚,我只是想把一般人忽视的地方尽自己所能描述一下.权当班门弄斧,贻笑大方了. 首先我想先从C和C++的一些基本概念入手. main()函数 稍微学过C和C++的人都知道main()函数市所有C和C++程序必不可少的东

  • 基于线程、并发的基本概念(详解)

    什么是线程? 提到"线程"总免不了要和"进程"做比较,而我认为在Java并发编程中混淆的不是"线程"和"进程"的区别,而是"任务(Task)".进程是表示资源分配的基本单位.而线程则是进程中执行运算的最小单位,即执行处理机调度的基本单位.关于"线程"和"进程"的区别耳熟能详,说来说去就一句话:通常来讲一个程序有一个进程,而一个进程可以有多个线程. 但是"任务

  • Java 虚拟机(JVM)之基本概念详解

    1.类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间. 2.方法区:就是存放类信息.常量信息.常量池信息.包括字符串字面量和数字常量等.方法区是辅助堆栈的块永久区,解决堆栈信息的产生,是先决条件. 3.Java堆:再java虚拟机启动的时候建立Java堆,它是java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享的.堆解决的是数据存储问题,即数据怎么放.放在哪儿. 4.直接内存:Java的NIO库允许Ja

  • Java分层概念详解

    service是业务层 action层即作为控制器 DAO (Data Access Object) 数据访问 1.JAVA中Action层, Service层 ,modle层 和 Dao层的功能区分?(下面所描述的service层就是biz) 首先这是现在最基本的分层方式,结合了SSH架构.modle层就是对应的数据库表的实体类. Dao层是使用了Hibernate连接数据库.操作数据库(增删改查). Service(biz)层:引用对应的Dao数据库操作,在这里可以编写自己需要的代码(比如简

  • 基于java中集合的概念(详解)

    1.集合是储存对象的,长度可变,可以封装不同的对象 2.迭代器: 其实就是取出元素的方式(只能判断,取出,移除,无法增加) 就是把取出方式定义在集合内部,这样取出方式就可以直接访问集合内部的元素,那么取出方式就被定义成了内部类. 二每一个容器的数据结构不同,所以取出的动作细节也不一样.但是都有共性内容判断和取出,那么可以将共性提取,这些内部类都符合一个规则Iterator Iterator it = list.iterator(); while(it.hasNext()){ System.out

  • java语言注解基础概念详解

    1.RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃: 2.RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期: 3.RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在: 这3个生命周期分别对应于:Java源文件(.java文件)--->.class文件--->内存中的字节码.

  • JavaScript事件概念详解(区分静态注册和动态注册)

    js中的事件 什么是事件?事件是电脑输入设备与页面进行交互的响应,我们称之为事件 事件类型 鼠标单击:例如单击button.选中checkbox和radio等元素:鼠标进入.悬浮或退出页面的某个热点:例如鼠标停在一个图片上方或者进入table的范围: 键盘按键:当按下按键或释放按键时: HTML事件:例如页面body被加载时:在表单中选取输入框或改变输入框中文本的内容:例如选中或修改了文本框中的内容: 突变事件:主要指文档底层元素发生改变时触发的事件,如DomSubtreeModified(DO

  • Java内存模型之happens-before概念详解

    简介 happens-before是JMM的核心概念.理解happens-before是了解JMM的关键. 1.设计意图 JMM的设计需要考虑两个方面,分别是程序员角度和编译器.处理器角度: 程序员角度,希望内存模型易于理解.易于编程.希望是一个强内存模型. 编译器和处理器角度,希望减少对它们的束缚,以至于编译器和处理器可以做更多的性能优化.希望是一个弱内存模型. ​因此JSR-133专家组设计JMM的核心目标就两个: 为程序员提供足够强的内存模型对编译器和处理器的限制尽可能少 ​下面通过一段代

  • C++string容器基本概念详解

    string基本概念 本质: string是C++风格的字符串,而string本质上是一个类 string和char*区别: char*是一个指针 string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器. 特点: string类内部封装了很多成员方法 例如:查找find,拷贝copy,删除delete,替换replace,插入insert string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责. string构造函数 #includ

  • springmvc分层领域模型概念详解

    目录 1.为什么出现分层领域模型这个东西? 2.分层领域模型有哪些? 3.分层领域模型的简单理解 3.1 VO和DTO的区别 3.2BO和DTO的区别 4.总结 本文核心为分层领域模型(VO , PO , BO, DAO ,POJO等)概念的个人理解. 1.为什么出现分层领域模型这个东西? (1)解决MVC架构中各层(比如视图层+控制层+服务层+数据访问层+数据库)中各层数据交互时,传递什么数据模型更加科学和合理. (2)更好的降低MVC架构中各层间的耦合性,提高层内的内聚性,这样更方便对软件进

随机推荐