CPlusPlus基础
学习依托C++那些事
const
定义:在声明的变量或参数前加上关键字const用于指明变量值不可修改(如const int foo),为类中的函数加上const限定表明函数(常成员函数)不会修改类成员变量的状态( class Foo {int Bar(char c) const; }; )。
优点:
-
便于编译器进行类型检测:
const常量与#define宏定义常量的区别:~~const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查。
-
防止修改,起保护作用,增加程序稳健型;
-
可以节省空间,避免不必要的内存分配,同时提高效率;
const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态),而#define 定义的宏常量在内存中有若干个拷贝。
#define M 3 //宏常量
const int N=5; //此时并未将N 放入内存中
......
int i=N; //此时为N 分配内存,以后不再分配!
int I=M; //预编译期间进行宏替换,分配内存
int j=N; //没有内存分配
int J=M; //再进行宏替换,又一次分配内存!
缺点:如果你向一个函数传入const 变量,函数原型中也必须是const 的(否则变量需要const_cast 类型转换),在调用库函数时这尤其是个麻烦。
void InputInt(int * num)
{
cout<<*num<<endl;
}
int main()
{
const int constant = 21;
//InputInt(&constant); //error C2664: “InputInt”: 不能将参数 1 从“const int”转换为“int *”
InputInt(const_cast<int*>(&constant));
system("pause");
}
结论:const 变量、数据成员、函数和参数为编译时类型检测增加了一层保障,更好的尽早发现错误。因此,我们强烈建议在任何可以使用的情况下使用const:
- 如果函数不会修改传入的引用或指针类型的参数,这样的参数应该为const;
- 尽可能将函数声明为const,访问函数应该总是const,其他函数如果不会修改任何数据成员也应该是const,不要调用非const 函数,不要返回对数据成员的非const 指针或引用;
- 如果数据成员在对象构造之后不再改变,可将其定义为const。
const修饰指针
const char * a; //指向const对象的指针或者说指向常量的指针。
char const * a; //同上
char * const a; //指向类型对象的const指针。或者说常指针、const指针。
const char * const a; //指向const对象的const指针。
const修饰函数名
概念:当const在函数名前⾯的时候修饰的是函数返回值,在函数名后⾯表⽰是常成员函数,该函数不能修改对象内的任何成员,只能发⽣读操作,不能发⽣写操作。
在一个类中,任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
原理:⼀个事实是,在调⽤成员函数的时候编译器会将对象⾃⾝的地址作为隐藏参数传递给函数。在const成员函数中,既不能改变this所指向的对象,也不能改变this所保存的地址,所以this的类型是⼀个指向const类型对象的const指针。
⼀个函数名字后有const,这个函数必定是成员函数,也就是说普通函数后⾯不能有const修饰。
如果定义了⼀个类的const对象(⾮const对象可以调⽤const成员函数和⾮const成员函数 ),它只能调⽤类中的const成员函数。
突破const属性:如果要去掉函数的const属性,可以声明关键字mutable,它允许const函数体内修改被mutable声明的变量的值。注意:关键字mutable 可以使用,但是在多线程中是不安全的,使用时首先要考虑线程安全。
如:
// mutable.cpp
class X {
public:
bool GetFlag() const {
m_accessCount++;
return m_flag;
}
private:
bool m_flag;
mutable int m_accessCount;
};
const 修饰函数参数
- 输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。
- 对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void func(A a) 改为void func(const A &a)。
- 对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void func(int x) 不应该改为void func(const int &x)。
const修饰类中的变量
在⼀个类中,任何不会修改数据成员的函数都应该声明为const类型,提⾼程序的健壮性。
对于类中的const成员变量必须通过初始化列表进⾏初始化,即初始化列表是先于构造函数的函数体执⾏,如下所⽰:
class Apple {
private:
int people[100];
public:
Apple(int i);
const int apple_number;
};
Apple::Apple(int i):apple_number(i) {}
除了上述的初始化const常量⽤初始化列表⽅式外,也可以通过下⾯⽅法:
1、将常量定义与static结合; 2、外⾯初始化。C++11
static const int apple_number = 10; // case1
const int Apple::apple_number = 10; // case2
这⾥提到了static,下⾯简单的说⼀下:
在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现⽂件中
初始化。
“.h”
static int ap;
“.cpp”
int Apple::ap=666
static
static修饰变量
第一个作用:修饰变量。变量又分为局部和全局变量,但它们都存在内存的静态区。
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用extern 声明也没法使用他。准确地说作用域是从定义之处开始,到文件结尾处结束,在定义之处前面的那些代码行也不能使用它。
静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了。由于被static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。
static修饰函数
第二个作用:修饰函数。函数前加static 使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。
static在类中的使用
作用于类的成员,解决同一个类的不同对象之间数据和函数共享问题
作用于类的数据成员,使其成为静态数据成员
静态成员在每一个类中只有一个副本,由该类所有对象共同维护和使用,从而实现同一个类的不同对象数据共享。
对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使⽤构造函数初始化。静态变量初始化需要注意:
- 初始化在类体外进⾏,⽽前⾯不加static,(这点需要注意)以免与⼀般静态变量或对象相混淆;
- 初始化时使⽤作⽤域运算符来标明它所属类,因此,静态数据成员是类的成员,⽽不是对象的成员。
class Apple {
public:
static int i;
};
int Apple::i = 1;
int num = Apple::i; // 引⽤静态数据成员⽅法
作用于类的函数成员,使其成为静态函数成员
静态成员函数就是使用static关键字声明的函数成员,同静态数据成员一样,静态成员函数也属于整个类,由该类所有对象共同拥有,为所有对象共享
class Apple {
public:
// static member function
static void printMsg() {
cout<<"Welcome to Apple!";
}
};
// main function
int main() {
// invoking a static member function
Apple::printMsg();
}
this
this指针的用处
- 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果;
- this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译时也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
this指针的使用
- 在类的非静态成员函数中返回类对象本身的时候,直接使用return * this。
- 当参数与成员变量名相同时,如
this->n = n
(不能写成n = n)。 - this会被编译器解析成A *const形式。当this指向常成员函数时,会被解析成
const A *const
。因为这个函数是const函数,那么针对const函数,它只能访问const变量与const函数,不能修改其他变量的值,所以需要一个this指向不能修改的变量,那就是const A *
,又由于本身this是const指针,所以就为const A *const
。
总结
- this在成员函数开始执行前构造,在成员执行结束后清楚;
- 在C++中类和结构只有一个区别:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针。struct和class最基本的区别是默认的访问权限:struct作为数据结构的实现体,默认是public的,class作为对象的实现体,默认是private的。
inline
内联的使用方式
**实现:**定义了的函数是隐式内联函数,声明要想成为内联函数,必须在实现处(定义处)加inline关键字。inline要起作用,inline要与函数定义放在一起,inline是一种“用于实现的关键字,而不是用于声明的关键字”。
**定义:**当函数被声明为内联函数之后,编译器可能会将其内联展开,无需按通常的函数调用机制调用内联函数。
**优点:**当函数体比较小的时候,内联该函数可以令目标代码更加高效。对于存取函数(accessor、mutator)以及其他一些比较短的关键执行函数。
**缺点:**滥用内联将导致程序变慢,内联有可能是目标代码量或增或减,这取决于被内联的函数的大小。内联较短小的存取函数通常会减少代码量,但内联一个很大的函数(译者注:如果编译器允许的话)将戏剧性的增加代码量。在现代处理器上,由于更好的利用指令缓存(instruction cache),小巧的代码往往执行更快。
**结论:**一个比较得当的处理规则是,不要内联超过10 行的函数。对于析构函数应慎重对待,析构函数往往比其表面看起来要长,因为有一些隐式成员和基类析构函数(如果有的话)被调用!
**另一有用的处理规则:**内联那些包含循环或switch 语句的函数是得不偿失的,除非在大多数情况下,这些循环或switch 语句从不执行。
重要的是,虚函数和递归函数即使被声明为内联的也不一定就是内联函数。通常,递归函数不应该被声明为内联的(译者注:递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数);虚函数表现为多态时,是不能内联的。
纯虚函数与抽象类
**定义:**C++中的纯虚函数(或抽象函数)是我们没有实现的虚函数!我们只需声明它!通过声明中赋值0来声明纯虚函数!
- 纯虚函数:没有函数体的虚函数;
- 抽象类:包含纯虚函数的类;
// 文件MavMessengerInterface.h
#ifndef MAV_MESSENGER_INTERFACE_H
#define MAV_MESSENGER_INTERFACE_H
#include <mavlink_headers/ardupilotmega/mavlink.h>
class MavMessengerInterface
{
public:
virtual ~MavMessengerInterface(){};
//return true if the message is sent
//false is an has error occured
virtual bool send_message(mavlink_message_t &msg) = 0;
//add a MavMessengerInterface to the listener list
//listeners are sent any message that is received by this MavMessengerInterface
virtual void append_listener(MavMessengerInterface* listener) = 0;
//start internal thread(s) that read interface continuously
//no effect if implementation has no thread
virtual void start() = 0;
//join internal thread
//return immediately if implementation has no thread (ex: logger)
virtual void join() = 0;
};
#endif
// 此类为抽象类,其中virtual修饰的函数为纯虚函数;
抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!
抽象类
抽象类中:在成员函数内可以调用纯虚函数,在构造函数/析构函数内部不能使用纯虚函数。
如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。
重要点
-
纯虚函数使一个类变成抽象类;
-
抽象类类型的指针和引用 -> 由抽象类派生出来的类的对象,不能创建抽象类的对象;
-
如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类;
-
抽象类可以有构造函数;
-
抽象函数的构造函数不能是虚函数,而祈构函数可以是虚祈构函数;
当基类指针指向派生类对象并删除对象时,我们可能希望调用适当的祈构函数。如果祈构函数不是虚拟的,则只能调用基类祈构函数。
// 文件MavlinkLogWriter.h
#ifndef MAVLINK_LOG_WRITER_H
#define MAVLINK_LOG_WRITER_H
#include <mavkit/MavMessengerInterface.h>
#include <thread>
#include <mutex>
#include <vector>
#include <string>
class MavlinkLogWriter : public MavMessengerInterface
{
public:
MavlinkLogWriter(std::string log_path);
~MavlinkLogWriter();
bool send_message(mavlink_message_t &msg);
void append_listener(MavMessengerInterface* listener);
void start();
void join();
private:
std::mutex mutex;
int create_log_files(std::string path);
int in_fd, out_raw_fd, out_ts_fd;
struct timespec start_time;
};
#endif
// 此类为抽象类MavMessengerInterface的派生类,并且包含了所有的纯虚函数,抽象类MavMessengerInterface的祈构函数是纯虚的。
virtual
虚函数与运行多态
虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
虚函数中默认参数
默认参数是静态绑定的,虚函数是动态绑定的。 默认参数的使用需要看指针或者引用本身的类型,而不是对象的类型。
总结:函数的执行取决于对象的类型,默认参数取决于引用本身。
重要点
**(1)静态函数可以声明为虚函数吗?**不可以
静态函数不可以声明为虚函数,同时也不能被const和volatile关键字修饰。
static成员函数不属于任何对象或类实例,所以即使给此函数加上virtual也是没有任何意义。(类名和范围解析运算符::调用静态成员)
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
**(2)构造函数可以为虚函数吗?**不可以
构造函数不可以声明为虚函数。同时除了inline|explicit之外,构造函数不允许使用其它任何关键字。
为什么构造函数不可以为虚函数?
尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
**(3)析构函数可以为虚函数吗?**可以
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。 事实上,只要一个类有可能会被其它类所继承, 就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。
**(4)虚函数可以为私有函数吗?**可以
- 基类指针指向继承类对象,则调用继承类对象的函数;
- int main()必须声明为Base类的友元,否则编译失败。 编译器报错: ptr无法访问私有函数。 当然,把基类声明为public, 继承类为private,该问题就不存在了。
#include<iostream>
using namespace std;
class Derived;
class Base {
private:
virtual void fun() { cout << "Base Fun"; }
friend int main();
};
class Derived: public Base {
public:
void fun() { cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived;
ptr->fun();
return 0;
}
修改为:
#include<iostream>
using namespace std;
class Derived;
class Base {
public:
virtual void fun() { cout << "Base Fun"; }
// friend int main();
};
class Derived: public Base {
private:
void fun() { cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived;
ptr->fun();
return 0;
}
**(5)虚函数可以被内联吗?**可以
通常类成员函数都会被编译器考虑是否进行内联。 但通过基类指针或者引用调用的虚函数必定不能被内联。 当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
return 0;
}
RTTI与dynamic_cast
RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
在面向对象程序设计中,有时我们需要在运行时查询一个对象是否能作为某种多态类型使用。与Java的instanceof,以及C#的as、is运算符类似,C++提供了dynamic_cast函数用于动态转型。相比C风格的强制类型转换和C++ reinterpret_cast,dynamic_cast提供了类型安全检查,是一种基于能力查询(Capability Query)的转换,所以在多态类型间进行转换更提倡采用dynamic_cast。
#include<iostream>
#include<typeinfo>
using namespace std;
class B { virtual void fun() {} };
class D: public B { };
int main()
{
B *b = new D; // 向上转型
B &obj = *b;
D *d = dynamic_cast<D*>(b); // 向下转型
if(d != NULL)
cout << "works"<<endl;
else
cout << "cannot cast B* to D*";
try {
D& dobj = dynamic_cast<D&>(obj);
cout << "works"<<endl;
} catch (bad_cast bc) { // ERROR
cout<<bc.what()<<endl;
}
return 0;
}
// dynamic_cast 转换类型必须具备多态
volatile
定义
被 volatile
修饰的变量,在对其进行读写操作时,会引发一些可观测的副作用(volatile
修饰的指针指向的内存可能是硬件设备,从p指向的内存读取数据可能会伴随可观测的副作用:硬件状态的修改)。而这些可观测的副作用,是由程序之外的因素决定的。
- likely:可能的。
- suddenly:突然的。
- unexpectedly:不可预期的。
因此,volatile 其实就是告诉我们,被它修饰的对象出现任何情况都不要奇怪,我们不能对它们做任何假设。
volatile应用
(1)并行设备的硬件寄存器(如状态寄存器)。 假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
int *output = (unsigned int *)0xff800000; //定义一个IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++)
{
*output = i;
}
}
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为 9,所以编译器最后给你编译的代码结果相当于:
int init(void)
{
*output = 9;
}
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
(2)一个中断服务子程序中访问到的变量;
static int i=0;
int main()
{
while(1)
{
if(i) dosomething();
}
}
/* Interrupt service routine */
void IRS()
{
i=1;
}
上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
(3)volatile
与多线程
多线程应用中被多个任务共享的变量。 当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:
volatile bool bStop=false; //bStop 为共享全局变量
//第一个线程
void threadFunc1()
{
...
while(!bStop){...}
}
//第二个线程终止上面的线程循环
void threadFunc2()
{
...
bStop = true;
}
要想通过第二个线程终止第一个线程循环,如果bStop不使用volatile定义,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
注:此处的关于多线程的处理方式在某些情况下是不合适的。
volatile
不能解决多线程中的问题。volatile
只在三种场合下是合适的:- 和信号处理(signal handler)相关的场合;
- 和内存映射硬件(memory mapped hardware)相关的场合;
- 和非本地跳转(setjmp和longjmp)相关的场合。
例:多线程同步中的一个基本问题:happens-before。
// global shared data
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
两个方面(编译器可能会做两个方面的优化):其一,在thread1中,编译器可能会将if(flag == true)
的内容全部优化掉。其二,在thread2中,编译器可能会调换update
和flag = true
语句的执行顺序,并且CPU的乱序执行的特点,也导致其执行顺序的不确定。
**解决问题思考过程一:**在变量前加volatile
,可以解决第一个问题,无法解决第二个问题。
// global shared data
volatile bool flag = false; // 1.
**解决问题思考过程二:**在value变量前加volatile仍然无法解决CPU乱序执行导致的执行顺序不确定的问题,并且可能会引发陈鼓型崩溃。
**解决问题思考过程三:**利用原子操作,由于对 std::atomic<bool>
的操作是原子的,同时构建了良好的内存屏障,因此整个代码的行为在标准下是良定义的。
// global shared data
std::atomic<bool> flag = false; // #include <atomic>
**解决问题思考过程四:**利用互斥量和条件变量。
// global shared data
std::mutex m; // #include <mutex>
std::condition_variable cv; // #include <condition_variable>
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [](){ return flag; });
apply(value);
lk.unlock();
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
std::lock_guard<std::mutex> lk(m);
// do some evaluations
value->update(/* parameters */);
flag = true;
cv.notify_one();
return;
}
这样一来,由线程之间的同步由互斥量和条件变量来保证,同时也避免了 while (true)
死循环空耗 CPU 的情况。
是否了解volatile的应用场景是区分C/C++程序员和嵌入式开发程序员的有效办法,搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,这些都要求用到volatile变量,不懂得volatile将会带来程序设计的灾难。
volatile常见问题
(1)一个参数既可以是const还可以是volatile吗?为什么? 可以。
一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
(2)一个指针可以是volatile吗?为什么? 可以。尽管这并不常见。(不甚理解)
一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时。
(3)下面的函数有什么错误?
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
这段代码有点变态,其目的是用来返回指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a=*ptr;
return a * a;
}
volatile总结
- volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
- volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
- const 可以是 volatile (如只读的状态寄存器)
- 指针可以是 volatile
assert——函数的入口校验
是宏,而非函数,包含在assert.h 头文件中。这个宏只在Debug 版本上起作用,而在Release 版本被编译器完全优化掉,这样就不会影响代码的性能。
不管什么时候,我们使用指针之前一定要确保指针是有效的。一般在函数入口处使用assert(NULL != p)
对参数进行校验。在非参数的地方使用if(NULL != p)
来校验。但这都有一个要求,即p
在定义的同时被初始化为NULL
了。
断言主要用于检查逻辑上不可能的情况。例如,它们可用于检查代码在开始运行之前所期望的状态,或者在运行完成后检查状态。与正常的错误处理不同,断言通常在运行时被禁用。
# define NDEBUG // 忽略断言 在include之前
#include<assert.h>
int main(){
int x=7;
assert(x==5);
return 0;
}
位域bit field
“ 位域 “ 或 “ 位段 “(Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。
struct bit_field_name
{
type member_name : width;
};
Elements | Description |
---|---|
bit_field_name | 位域结构名 |
type | 位域成员的类型,必须为 int、signed int 或者 unsigned int 类型 |
member_name | 位域成员名 |
width | 规定成员所占的位数 |
注:
- 位域设置的位数超过变量类型的最大值,编译器会自动将变量移位至下一个类型单元存放。
- 宽度为 0 的未命名位域成员令下一位域成员与下一个整数对齐。
- 位域的初始化可以直接赋值,类似结构体赋值,或者可以利用指针或联合体union进行位重映射。
extern
(1) C++调用C函数
//xx.h
extern int add(...)
//xx.c
int add(){
}
//xx.cpp
extern "C" {
#include "xx.h"
}
(2) C 调用C++函数
//xx.h
extern "C"{
int add();
}
//xx.cpp
int add(){
}
//xx.c
extern int add();
struct
C和C++中的Struct区别
C | C++ |
---|---|
不能将函数放在结构体声明 | 能将函数放在结构体声明 |
在C结构体声明中不能使用C++访问修饰符。 | public、protected、private 在C++中可以使用。 |
在C中定义结构体变量,如果使用了下面定义必须加struct。 | 可以不加struct |
结构体不能继承(没有这一概念)。 | 可以继承 |
若结构体的名字与函数名相同,可以正常运行且正常的调用! | 若结构体的名字与函数名相同,使用结构体,只能使用带struct定义! |
struct与class区别
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:
最本质的一个区别就是默认的访问控制
默认的继承访问权限。struct 是 public 的,class 是 private 的。
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
union
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
union two_bytes {
uint16_t uint;
uint8_t bytes[2];
};
union four_bytes {
uint32_t uint32;
float_t float32;
};
explicit
只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化,防止类构造函数的隐式自动转换.
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
class CxString // 没有使用explicit关键字的类声明, 即默认为隐式声明
{
public:
char *_pstr;
int _size;
CxString(int size)
{
_size = size; // string的预设大小
_pstr = malloc(size + 1); // 分配string的内存
memset(_pstr, 0, size + 1);
}
CxString(const char *p)
{
int size = strlen(p);
_pstr = malloc(size + 1); // 分配string的内存
strcpy(_pstr, p); // 复制字符串
_size = strlen(_pstr);
}
// 析构函数这里不讨论, 省略...
};
// 下面是调用:
CxString string1(24); // 这样是OK的, 为CxString预分配24字节的大小的内存
CxString string2 = 10; // 这样是OK的, 为CxString预分配10字节的大小的内存
CxString string3; // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码
string1 = 2; // 这样也是OK的, 为CxString预分配2字节的大小的内存
string2 = 3; // 这样也是OK的, 为CxString预分配3字节的大小的内存
string3 = string1; // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用free释放_pstr内存指针的时候可能会报错, 完整的代码必须重载运算符"=", 并在其中处理内存释放
class CxString // 使用关键字explicit的类声明, 显示转换
{
public:
char *_pstr;
int _size;
explicit CxString(int size)
{
_size = size;
// 代码同上, 省略...
}
CxString(const char *p)
{
// 代码同上, 省略...
}
};
// 下面是调用:
CxString string1(24); // 这样是OK的
CxString string2 = 10; // 这样是不行的, 因为explicit关键字取消了隐式转换
CxString string3; // 这样是不行的, 因为没有默认构造函数
CxString string4("aaaa"); // 这样是OK的
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
CxString string6 = 'c'; // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换
string1 = 2; // 这样也是不行的, 因为取消了隐式转换
string2 = 3; // 这样也是不行的, 因为取消了隐式转换
string3 = string1; // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载
explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数。