程序员肖邦的博客 享受编程和技术所带来的快乐

C++编程基础知识点

2015-04-15
肖邦
C++

C++ 编程基础知识点总结。

C++编程基础知识点

  • 基本介绍
  • 第一个 C++ 程序
  • 名字空间
  • 结构、联合、枚举
  • 布尔类型
  • 操作符别名
  • 重载
  • 缺省参数和哑元
  • 内联
  • 动态内存分配
  • 引用
  • 显式类型转换
  • 来自 C++ 社区的建议

一、基本介绍

1、C++ 的发展

  • 80 年代,贝尔实验室发明 C with Classes — 本贾尼
  • 1983 年,正式命名为 C++
  • 1987 年,GNU 推出 C++ 的标准
  • 1992 年,微软、IBM 推出他们的标准
  • 1998 年,ISO 推出 C++98 标准
  • 2003 年,ISO 推出 C++03 标准
  • 2011 年,ISO 推出 C++0x 标准

2、C++ 和 C 语言的联系和区别

  • C++ 包含整个 C,C++ 是建立在 C 语言基础上的
  • C++ 是强类型语言,比 C 语言对类型检查更加严格
  • C++ 语言比 C 语言更加丰富
  • C++ 支持面向对象( C++ 在宏观上面向对象,微观上是面向过程)
  • C++ 支持运算符重载
  • C++ 支持异常处理
  • C++ 支持泛型编程(类型通用编程)

二、第一个C++程序

1、编译器 g++

也可以用 gcc,但要加上 -lstdc++,g++ 其实就是 gcc,gcc 是一组程序,包括 g++

$ gcc first.cpp -lstdc++

2、扩展名

常用扩展名:.cpp | .cc | .C | .cxx 推荐使用.cpp。也可以用.c,但是要加上 -x c++

$ gcc first.c -x c++

3、头文件

  • 头文件位置: /usr/include/c++/4.x
  • C++ 标准的头文件没有.h(与 C 的头文件没有本质区别):
  • C 的头文件在 C++ 中使用:
  • 使用系统的头文件(非标 C 头文件): <sys/types.h>

4、流操作

cout << "Hello World.";
cin >> a;
// 也可以使用 scanf 和 printf

5、使用 std 空间下的相关数据

所有标准类型、对象和函数都位于std命名空间中

std::cout       /* 标准输出对象 */
std::endl       /* 标准结束行的对象 */
std::cin        /* 标准的输入对象 */
std::cout << "hello world" << std::endl;

前面加空间名,是最根本的使用方式

6、使用声明的方式

使用声明的方式,能简化一个对象的使用

using std::cout;
using std::endl;
cout << "hello world" << endl;

7、使用指令的方式

使用指令的方式,能简化多个对象的使用

using namespace std;
cout << "hello world" << endl;

三、名字空间

1、为什么需要名字空间(why)

  • 划分逻辑单元
  • 避免名字冲突

2、什么是名字空间(what)

  • 名字空间定义:把一组相关的数据,放入一个逻辑结构中统一管理
  • 名字空间合并
  • 声明定义分开
  • 声明和定义分开后,编译器可以帮助捕捉到例如拼写错误或类型不匹配一类的错误

3、怎样用名字空间(how)

  • 作用域限定符 “::”
  • 名字空间指令: 本空间中所有的标识符在该指令之后都可见(可以直接使用)
  • 名字空间声明: 空间中声明过的标识符可以直接使用
  • 注:局部、全局、名字空间(可以看作是作用域,限制变量、函数等的使用范围)

4、无名名字空间(匿名名字空间)

  • 不属于任何有名名字空间的标识符,隶属无名命名空间
  • 无名名字空间的成员,直接通过 :: 访问(作用域限定符)
 namespace {   /* 无名名字空间 */
    int a=15;
    ... ...
    void test(){
    }
}
::test();   /* 只有加上::才代表调用匿名空间的数据 */

5、名字空间嵌套与名字空间别名

  • 内层标识符隐藏外层同名标识符
  • 嵌套的名字空间需要逐层分解
  • 可通过名字空间别名简化书写
  • namespace ns_four = ns1::ns2::ns3::ns4;

四、C++结构体、联合、枚举

1、C++结构体

  • 声明或定义结构型变量,可以省略struct关键字
  • 可以定义成员函数,在结构体的成员函数中可以直接访问该结构体的成员变量,无需通过.->
  • 其实函数的存储区不在堆栈,仍在代码区,放在一块使用方便,将数据和操作封装
  • C++ 中没有任何成员的结构体大小是 1 (C 语言的是 0)
  • C++ 中的structclass只有一个区别: struct 的默认权限为 public,class 的默认权限是 private
  • C++ 中struct的存在是为了兼容 C 语法,推荐在 C++ 中尽可能少的使用struct, 而使用class
  • C++ 与 C 中的struct从思想上完全不是一个东西,C 中主要是数据结构,C++ 中主要是面向对象的类

2、C++联合

  • 多个变量共享一段内存
  • 声明或定义联合变量,可以省略union关键字
  • 支持匿名联合
  • C++ 匿名联合并不表示某种类型,用来表示联合中数据在内存中的布局形式
  • 计算机(大端、小端序列,网络序列永远是大端序列)——小低低(计算机字节序)
union var{
    char c[4]; int i;
};
union var data;
data.c[0] = 0x04; data.c[1] = 0x03;
data.c[2] = 0x02; data.c[3] = 0x11; 
printf("%x\n",data.i);  // 数组中下标低的,地址也低 结果为:11020304
// 第 2 种方式:
union var{ int data; char i; }test;
test.data = 1; if (test.i == 1) printf("little endian");

3、C++枚举

  • 声明或定义枚举型变量,可以省略enum关键字
  • 枚举本质上是整数,所以枚举可以赋值给 int,但 C++ 类型检查严格,int 值不能赋值枚举变量

五、参数与零值

1、C++零值

只有 NULL'\0'0false 表示 0 值(假)

2、C++无参

  • C 语言中无参代表可以有任意个参数,void 代表无参
  • C++ 中无参代表没有任何参数,void 可以继续使用
  • C++ 函数在调用前必须声明,不再支持 C 语言的隐式声明
  • C++ 不再支持(C 语言函数不设计返回值,默认返回 int),main 函数除外

六、函数重载

1、函数重载相关概念

  • 同一作用域中,函数名相同,参数表不同的函数,且同作用域中对其可见才能构成函数重载
  • 不同作用域中同名函数遵循标识符隐藏原则
  • 参数列表表的类型、数量、顺序,与返回值无关
  • 重载的优点:对函数的实现者,函数的名字简单;对函数的调用者,不用考虑类型,方便调用

2、函数重载的原理

  • C++ 重载是通过换名实现的,编译器会根据函数名和参数列表的不同,生成新的函数名,本质的机制与 C 一样
  • 通过 extern “C” 可以要求 C++ 编译器按照 C 方式处理函数接口,即不做换名,当然也就无法重载

3、函数重载解析

  • 函数重载是根据参数列表来选择,有时列表并非完全匹配,编译器根据匹配度进行评分,然后根据分数选择分数最高的匹配
  • 完全匹配 -> 常量转换 -> 升级转换 -> 标准转换 -> 自定义转换 -> 省略号匹配
  • 完全匹配: 是编译器的最佳选择方案,首选
  • 常量转换: 是将非常量参数,转换为常类的参数,变得更安全,匹配度相对较高
  • 升级转换: char ->int 是升级转换,从 1 字节转换到 4 字节,更安全,但能转换 int 就不会转换 long long int
  • 标准转换: double -> int 会有小数点后边数据丢失,数据损失; int -> double 会有精度损失的; 两个都属于标准转换
  • 省略号转换:匹配度最低,因为它是可变长参数,最不可靠的
  • 通过示例程序理解 C++ 重载原理
extern "C" {
    int add(int a,int b){
        return a+b;
    }
    double add(double a,double b){
        return a+b;
    }   /* 编译出错,没有换名,不能构成重载关系 */
}       /* 大括号内所有的 g++ 编译都不改名 */

4、C++与C的相互调用

  • 重载导致 C 和 C++ 相互调用会出现链接时名字不匹配,而发生调用错误

C调用C++代码:

a. 定义一个头文件: mymath.h 不需要 extern "C"
b. 编写被调函数的实现: mymath.cpp extern "C" int add(int a,int b) { ... }
c. 编写主调函数 main 并调用,调用 cpp 函数,加上头文件可以
d. g++ test.cpp -c -otest; gcc main.c -omain test; ./main

C++调用C代码:

a. 定义一个头文件: mymath.h 需要加 extern "C" int add(int a,int b);
b. 编写被调函数的实现: mymath.c 不需要加extern "C",因为 .c 函数本身生成代码不会改名字
c. 编写主调函数 main.cpp 并调用,加上 include "mymath.h" 因为头文件中声明了 extern "C";
   所以 main.cpp 与被调函数生成代码编译链接时都不会改名字,可以正常调用、链接使用

七、缺省参数和哑元

1、缺省参数

  • 可以为函数的参数指定缺省值
  • 如果函数的声明和实现是分开的,缺省值只能是在函数声明中指定,否则编译器报错
  • 函数参数缺省值是在编译时确定的,遇到函数调用语句时,编译器将用缺省的实参对应的形参缺省值替换
  • 如果函数的某一个参数具有缺省值,那么该参数后面所有参数必须都具有缺省值
  • 不要因为缺省参数而导致重载歧义

2、哑元

  • 只指定类型而不指定名称的函数参数,称为哑元
  • 保证函数的向下兼容
  • 形成函数的重载版本
  • 作用: 向前兼容、区别函数
  • 新老用户都能用,老用户仍然可以在调用函数中写参数,新用户就会明白这是哑元,不会对这个参数不理解

实例1(重载版本):

// 需求:一个是对两个整数求和,一个是 2 个整数求差,但要求函数名相同
int cal (int x, int y){
    return x + y;
}
int cal (int x, int y, int){
    return x - y;
}

实例2:

int operator++();           /* 默认表达前++ */
int operator++(int);        /* 这是后++ */

八、内联函数

  • 内联就是用函数已被编译好的二进制代码,替换对该函数的调用指令
  • 内联在保证函数特性的同时,避免了函数调用开销
  • 执行效率将会增高,它既有宏函数的高效特性,也有函数的类型特性
  • 只有频繁调用的简单函数才适合内联
  • 若函数在类声明中直接定义,则自动被优化为内联,否则可在其声明处加上 inline 关键字
  • inline 关键字仅表示期望该函数被优化为内联,但是是否适合内联则完全由编译器决定
  • 稀少被调用的复杂函数(>200 行)和递归函数都不适合内联

九、C++动态内存分配

  • new/delete 是运算符不是函数,功能是由编译器内置的。
  • 仍可以继续使用标准 C 库函数 malloc/calloc/realloc
  • 使用 new/delete 操作符在堆中分配/释放内存
    int *pi = new int; *pi = 1234; count << *pi << endl; delete pi;
    
  • 在分配内存的同时为内存初始化
    int *p = new int(999); count << *p << endl; delete p;
    
  • 以数组方式分配 new[],也要以数组方式释放 delete[]
    int *p = new int[4];    /* 分配内存不一定清0,视编译器而定 */
    delete[] p;             /* 释放内存时用 delete[],与 new 配对使用 */
    
  • 分配数组并初始化
    int *p = new int[4] {10, 20, 30, 40};
    delete[] p;
    

    分配数组并初始化必须要用 C++ 11 标准来支持,编译时 g++ new.cpp -std=c++0xstd=gnu+0x

  • 通过 new 操作符分配 N 维数组
    int (*p) [4] = new int[3][4];    // 返回数组指针
    int (*p) [4] = new int[3][4] {
      {1, 2, 3, 4}, 
      {5, 6, 7, 8}, 
      {9, 10, 11, 12}
    };
    
  • 释放内存 delete
    • 释放多次会出现 double free 的错误
    • 分配多少内存,就释放多少内存,不要试图只释放部分内存
    • 数组内存释放:delete[],不要忘记 []
    • 不能通过 delete 释放已释放过的内存
    • delete 野指针后果未定义,delete 空指针是安全的。
    • 内存分配失败,new 操作符抛出 bad_alloc 异常,一般原因是内存不足

十、C++的引用

  • 对变量的解释:
    • int a = 10。数据 10 是存在内存中,实际上变量是通过地址访问和存储的,我们在编程的时候用地址表示太麻烦,就给变量起了一个名字 a,是给程序员看的,在内存中不存储
    • 一般类型的返回值都是被存储在匿名变量中,而匿名变量都是作右值使用的
    • 表达式只能作为右值使用,不能作为左值给其赋值
  • 什么是引用?即变量的别名,就是变量的本身。
  • 如何定义引用
    int &b;             /* error,别名必须与实体是依附关系 */
    int &b = a;         /* 必须初始化 */
    const int &b = 10;  /* 可以常引用 */
    
  • 引用不能更换目标,不能为空
    • 一旦引用,终生为其目标服务
    • 类似于数组,数组名为常指针,不能更改,只能为所指向的内存服务
  • 引用型参数,参数的形参是实参的别名
    • 在函数中修改实参的值
    • 避免对象复制的开销
    • const 引用:防止意外修改参数
  • 常引用型参数
    • 防止对实参的意外修改
    • 在函数的形参中以引用的方式传值时,一般尽量加上 const 关键字
    • 用来接收常量型的实参,比如:show(10)
  • 引用型返回值,从函数中返回引用,一定要保证在函数返回后,该引用的目标依然有效
    • 用途一:把函数的返回值作为左值
    • 可以返回全局、静态以及成员变量的引用
    • 可以返回堆中动态创建的对象的引用
    • 可以返回调用对象自身的引用
    • 可以返回引用型参数本身
    • 不能返回局部变量的引用
  • 常引用型返回值,返回右值
  • 指针与引用的区别和联系
    • 引用本质上就是指针,在 C++ 语言层面,引用不是实体类型
    • 指针可以不初始化,目标能在初始化后随意更改(除非常指针),而引用必须初始化且初始化后无法更改
    • 存在空指针,不存在空引用(不能独立存在,必须依附一个实体)
    • 存在指向指针的指针,不存在引用引用的引用(不存在高级引用)
    • 存在引用指针的引用,不存在指向引用的指针
    • 存在指针数组,不存在引用数组,但存在数组引用
  • 数组名的使用时注意事项
    • 数组名大多情况下表示数组的首地址,以下三种情况代表的是整个数组
    • sizeof(arr) 表示整个数组的大小,但在函数参数中 sizeof(arr) 表示指针
    • &arr 的值表示等于 arr 作为地址时的值,但类型不是 int **,而是 int (*) [3]
    • int (&ra) [3] = arr arr 表示一个数组整体,并不代表首地址。在数组引用作为函数参数时,sizeof(arr) 值为整个数组的长度,非指针。注意与数组作为函数参数的区别。

十一、C++的显式类型转换

  • C 风格的显式类型转换
    int *p; char *buf = (char *)p;
    
  • C++ 风格的显式类型转换
    int *p; char *buf = char *(p);
    
  • 静态类型转换
    • void *p; int *buf = static_cast<void *>(p);
    • 其它类型指针可以隐式转换为 void* 类型,但 void* 不可以隐式转换别的类型
    • 隐式类型转换的逆转换
    • 自定义类型转换。如果在类型转换构造函数前加 explicit 表示进行显式类型转换,那么在进行对象类型转换构造时,需要显式的用 static_cast 进行转换
  • 动态类型转换
    • dynamic_cast<目标类型>(源类型变量)
    • 多态父子类指针或引用之间的转换
  • 常类型转换
    • const int *p1; int *p2 = const_cast<int *>(p1);
    • const_cast<目标类型>(源类型变量)
    • 去除指针或引用上的 const 属性,只能在同类型之间的数据类型转换
  • 重解释类型转换
    • reinterpret_cast<目标类型>(源类型变量)
    • 任意类型的指针或引用之间的转换。
    • 任意类型的指针和整数之间的转换。
    • C、C++ 的显式类型转换是编译器什么都不检查。
    • 静态类型转换是让编译器还能作一定程度上的检查。
  • 举例说明const_cast特殊性
    const int c = 100;
    int *p = const_cast<int *>(&c);
    *p = 200;
    cout << *p << endl;   // 200
    cout << c << endl;    // 结果:100。加 volatile 后结果为 200
    

    编译器会把 const 修饰的 c 变量进行优化,在以后的代码中遇到 c 变量就会以字面常量 10 去替换所有的 c 变量,增加 volatile 关键字后对 const 的优化取消,允许以其它方式改变 c 在内存的值,以后每次都从内存中直接取值,取消编译器的优化。

十二、来自C++社区的建议

  • const 取代常量宏
  • enum 取代唯一标识宏
  • inline 取代参数宏
  • namespace 取代条件解决名字冲突
  • 宏函数不能作为类的成员函数,不允许预处理器访问类的成员数据
  • 变量随用随声明,并立即初始化
  • 少用 malloc/freenew/delete 更好
  • 避免使用 void*、指针算术、联合、强制类型转换
  • 尽量少用数组和 C 风格字符串,标准库中的 stringSTL 容器可以简化程序
  • 树立面向对象的编程思想,程序设计的过程是用类描述世界的过程,而非用函数处理数据的过程,更不是一堆数据结构和一些底层的二进制数据

Comments

Content