港澳行记

我来也匆匆,去也匆匆。
                                    — 某歌词

记忆中和LP一起出去旅行的次数少的可怜,上一次还是在结婚蜜月时,去的是九寨。二人一起出游是很美妙的,印象也是深刻的,至今当时在九寨的情形 还能历历在目。于是年初就和LP定下了今年的一个家庭目标:一起出去玩一次。

不过真正要确定何时以及去哪出游还是很困难的,毕竟工作上的事情和照顾孩子的事情要安排妥当才行。上月中旬LP接到通知:端午前要到公司总部(广 州)接受培训,机会终于来了 – 都到了广州,怎能不去港澳呢!于是我们决定趁此机会去香港澳门玩一通,还能省下来一个人的路费^_^。LP以前是去过一次香港的,像迪斯尼、海洋公园、黄 大仙等景点LP都是游览过的,因此这次LP去香港以消费为主,我尾随并保驾护航。另外我们在是否带果果去这个问题上也纠结了一段时间,最后决定这 次就不带孩子去了。一方面孩子太小,带出去总是不那么放心,那边天气也热,怕她生病;另一方面这次以逛街为主,带孩子十分不便,孩子累,大人也 累。

行程确定

出游第一步:确定行程,否则没法订机票和酒店。和LP商议后,最终确定的行程:

6.9   广州自由行
6.10 入港,购物
6.11 香港,购物
6.12 从香港入澳门,逛景点,买点纪念品,晚上通过珠海拱北口岸回大陆,连夜回到广州,入住酒店
6.13 飞回沈阳

各种准备

LP两年前和同事结伴去过一次HongKong,但以景点旅游为主,对香港购物区了解甚少,因此这次出发前我们还是要做好充分的准备。

    – 订香港酒店。由于到港时恰逢端午假日,因此酒店的价格也是水涨船高,多亏我们提前近20天就预订了(LP在淘宝订的),这才勉强订到价格还算合理,且位置 和质量都属上乘的 – 郎豪酒店(Langham Place Hotel),郎豪酒店离九龙旺角地铁站出口很近,酒店下面就是香港的一个购物中心郎豪坊,酒店是五星级的,一天700RMB,感觉超值。

    – 订往返机票。由于LP只能从广州出发,因此只能订到广州的往返。6月恰逢广交会刚结束,机票折扣较大,因此往返定的都是2.5-3折左右的机票,已经是相 当的划算了。来回算上机场建设费、燃油费才不到1600。

    – 预订广州酒店。我和LP在广州要住上2宿,在网上找了很长时间,发现7天连锁今年有新会员“77元”自主大床房的体验活动,这个价格太合适了。于是和LP 每人注册了一个ID,在7天的广州动物园2分店预订了两天住宿(其他分店要么太远,要么就是客满,无法预订,便宜也有便宜的不足之处)。

    – 买广九直通车车票。坐大巴入港太慢,口岸等的时间也较长,因此我和LP决定坐火车进港。唯一的选择就是广九直通车,最早的一列是8点17左右的,到香港也 就10点多,很舒服方便,就是价格有些小贵,大概是坐Bus的两倍。这个票在12306.cn上是无法订到的,只能在窗口买,或在淘宝上找代理购 买。

    – 买拱北回广州的车票。从拱北回广州的Bus很多,Taobao上有很多代理提供车票服务。

    – 手机卡。这次没有购买香港当地的手机卡,由于客户级别已经在8级以上,开通国际漫游也无需押金,我就直接开通了国际漫游服务。

    – 选择目标购物区。香港的购物区有很多,我们在港也就待两个整天,因此我们需要确定每天的目标购物区。由于住在郎豪坊,因此我们第一天打算就在郎豪坊周围 逛。第二天去著名的海港城周围。

    – 各种打折卡和会员号。自从确定了要去香港,就一直在十六番论坛上潜水,搜罗一堆不知道是否好用的会员卡号,打印出来备用。

蒸笼上的广州

第一次去广州,也第一次感受到了广州的湿热,在大街上背个包走两步就浑身透汗。不识趣的我居然还跑到了越秀公园给五羊雕塑拍照,没想到越秀公园不 是“平原”,雕塑居然在小山上,这让我终于体会到什么叫汗流狭背了,一整包面巾纸都用来擦汗了。就这样走走歇歇,也算把越秀的主要景点逛了个遍。 越秀公园南门是中山纪念堂,我也顺便串了个门,给孙大总统的铜像拍了几张照片。广州的热还体现在即便是进入夜幕,热度依然不减。乘地铁回到住处, 不将空调开得凉凉的是无法入睡的。但考虑到不习惯开着空调睡觉,于是就开开关关,一宿也没咋睡好。

除了热之外,我对广州这座城市的印象还是蛮不错的。和大多数发展中的北方城市不同,广州人民,至少核心城区的人们已经开始享受发展后的成果了,尤 其是2010亚运会后的成果了 – 宽阔完好的道路、四通八达的交通系统、良好的绿化、鲜见灰尘的空气、发达的商业环境、先进的城市管理以及城区中数量众多的公园,这些至少比到处是工地、到 处修路、到处扬尘的沈阳要先进五年以上。没有尘土,马路上的车都是干净的,就算下雨也是没有泥的。

广州的吃食我还算是适应,云吞、肠粉、牛河、炒饭量足,价格也不是很贵,至少比北京是要便宜一些的。我没有特意去广州什么老字号或特色美食去体 验,我就是走到哪里吃到哪里。南方的甜品和汤我不是很感兴趣,所以也没有去体验。天河又一城下面的风行牛奶的西米露倒是买了一杯,感觉很是一般。

习惯了每到一地就去当地的博物馆看看,这次也不例外。广东省博物馆就在珠江北岸,离海心沙亚运公园很近,遥望南岸的广州地标 – 广州塔。博物馆是免费的,但门票最好提前一天预约。不过我去的那天人不是很多,不预约直接领票也很快。与LP一起逛博物馆只能走马观花了,那天博物馆的临 时展览包括“晋国遗珍——山西出土周代文物”、“金枝玉叶——明代江西藩王墓出土玉器精品展”、“静木清缘——金丝楠薄浮雕艺术展”,常设的展览 有:广东省自然资源展览、漆木精华——潮州木雕艺术展览等。个人十分喜欢“广东省自然资源展览”,里面展出了许多矿产、宝石等原始形态,十分开眼 界。

入港

10号早上和LP很从容的从酒店退房出来,坐地铁3站地到达广州东站,准备乘坐广九直通车入香港。直通车提前45分钟剪票,去香港的旅客(绝大多 数都是,应该没有人坐这趟车去东莞)要进行出境检查。直通车的车厢是类动车一等座席那种,但车头应该是普通的那种电力机车车头。车开的速度并不 快,尤其是通过深圳罗湖铁路口岸时车基本是龟速通过。过到香港这边后,火车与香港东铁线似乎走的是一条路线,只是沿路的车站不停罢了,直到到达香 港九龙红勘站。入境手续办理也不算慢,排一会就过去了。接下来要做的第一件事就是购买八达通卡,在香港没有八达通真是寸步难行啊。150港币一 张,离港前可退掉,但我和LP商量了,这次就不退票了,后续还会有很多机会来香港的,到时候只要重新激活一下八达通就可以使用了。

按照之前查找的路线,我们坐东铁线去旺角东站。坐了香港地铁才知道,香港地铁有些是地面上跑的轻铁,和普通火车没啥区别。出了旺角东站,我们就彻 底迷路了:分不清东南西北,周围是熙熙壤壤的人群,马路上穿梭着双层巴士,香港路牌还不是很适应。找人问路,问了几个居然还都是大陆游客。最后问 到一个本地人,告诉了我们去郎豪酒店的方向。我们就顺着走,感觉不对就再问,就这样误打误撞,还真的到了郎豪酒店对面。原来这里离旺角站更近,楼 下是一个H&M店,对面是西武百货,旁边有家翠华餐厅,似乎到了饭点,门口排着等位的人,估计都是慕名而来的大陆游客。

酒店一层有个很小的服务台,站着一位老外,估计是英国人。我们和他咨询如何check in,他似乎中文不是很地道,就用手比划出“八”的姿势,嘴里说什么"爱露",我们一直以为是到第8层办理check in手续,直到他送我们进电梯,我们才理解是到"L"层办理手续 – 电梯的按钮上明显标明了L – 酒店大堂。由于到的还早,酒店尚无房间,我们将行李做好寄存,就下楼开始逛香港。

消费天堂

郎豪酒店楼下就是郎豪坊,不过郎豪坊显然比较高档,很多店铺折扣都不大。我们估计不会在这里购物,于是下楼沿街逛。离郎豪坊不远处就是香港的一条 购物大道 – 弥敦道。在香港想辨别大路还是小道,就看路上双层Bus多少即可。郎豪酒店在弥敦道西侧,弥敦道东侧有通菜街(也就是著名的女人街)、奶路臣街等,这些街 道两侧林立着各类商铺,服装、化妆品、数码、珠宝、手表应有尽有,有些类似国内的步行街,但每个商铺面积都不大。这些街就是我和LP香港逛街的起 点了。

在香港逛街与在内地商场、专卖店逛街也没啥区别。并且在接待过多年大陆游客后,香港的各类店铺的服务员应对操持着普通话的大陆游客都是毫无问题 的。不过我还是总结了一些在香港逛街的几大特点:

   – 店铺冷气足
   香港店铺里的冷气不是一般的足,是足让你感觉到寒冷。游客一般都是短袖裤衩,但店员们都是衬衫西装,估计他们十分了解他们所处的温度环境。

   – 服务意识好
   多数店铺里的服务员服务的意识还是很不错的,不知道是否仅针对大陆游客^_^。据说香港本地人去买珠宝,一般都被要求“等会儿”。

   – 关联推销多
    我买了台ipad mimi,店员并不着急结帐,而是向你推销其他产品,比如外壳、贴膜;甚至是将你带到其他柜台,推销与ipad mini毫不相干的产品,比如剃须刀,这是我在百老汇亲历到的。在Nike/Adidas打折店买双运动鞋,她们也会不遗馀力的向你推销100元5双的运 动袜。

   – 珠宝行烂大街,以周大福为甚
    香港珠宝首饰店那叫一个多。以弥敦道为例,沿路两侧,五步一个周大福,十步一个周生生,再迈一步就是一个六福或谢瑞麟,丝毫不夸张。这里面还是以周大福的 店最为多,人气似乎也最旺。

   – 化妆品店林立
    由于免稅等原因,香港的化妆品的确多,而且种类丰富,从小样到套装应有尽有。卓悦、莎莎是香港最著名的化妆品零售店,而且满大街都是,十分方便。

   – 吃,一个也不能少
    网上谈的最多的就是翠华,也许是被大陆客炒起来的原因。我和LP在下午回酒店前去了一趟翠华餐厅,那时候人刚刚减少,排了2分钟就有了座位。点了咖喱牛腩 饭、番茄猪排饭,味道也就那么回事,没啥特殊的地方,价格“不菲”,一共117港币。
    反倒是香港的KFC和麦记相比翠华要便宜许多,我俩没少吃。像许留山这样的甜品店,就类似以前大陆“街客”那种地位。
 
   – 数码电器价格优势缩小
    除了apple系列产品,其他的数码产品在价格上比大陆优势不大,有些甚至比大陆的店还要贵,比如单反相机。佳能650D和700D在百老汇、丰泽和香港 苏宁的报价都比大陆要高出一些,遂放弃之。

下午三点左右,我们打算回酒店休息一下。行李已经被直接放到房间内了,这服务估计只有五星酒店才能享受到。酒店的房间比预象的要好上很多。房间设 计新颖,设备也很新,卫生间有个大浴缸,有电视声音输出,可以边泡澡边收看电视。各种商务设备我们都不是很了解,中央空调太冷,最高温度只能调到 26度,于是关之(触摸液晶屏的),总体感觉一宿700rmb还是值得的,毕竟这是HongKong。

休息后,继续逛街。这次沿着弥敦道一直向南逛,一路路过油麻地、九龙政府合署、佐敦、柏丽购物大道、尖沙嘴;调头逛了一下加连威老道,然后往回 走。当然这期间LP采购了好多东东,这里就不细数了。回到旺角时已经半夜11点多了,感觉好累。到KFC(24小时店)吃了一顿夜宵,回酒店休 息。

彻游海港城

11号早晨从CNN得知神十已经成功上天,甚感高兴,心情自然也不错。今天我们的目的地是海港城。之前就听说海港城巨大无比,一天都逛不完,于是乎早些起 来,整理物品,做出发准备。到海港城可以乘地铁,也可以坐双层Bus,我们选择体验后者 – 在弥敦道乘坐281/A路公交到中港城下车。香港的Bus都是比较舒适的,上下层都是软座,不像国内Bus那样拥挤(国情使然)。香港道路虽然不宽,但由 于大家基本遵守交通规则,所以路上的车开的都是飞快的。不过感觉随着大陆游客的增多,“中国式”过马路的现象在香港也越来越多了。

中港城在海港城的北侧,楼上是登船的闸口,楼下则是折扣店,虽然面积不大。由于到的比较早,就在中港城解决了早餐,并顺便到楼上打听一下第二天乘船到澳门 的事宜。多亏提前打听了一下,原来船票是有限的,我们打听的时候,第二天的船票都已经基本没了,于是赶紧用之前从taobao买到的换票券换了两张第二天 中午11点的到澳门的船票。

海港城是由多个商场组成的联体建筑,因此显得格外庞大,进去后都不知道该如何逛,即便拿到了导游图依旧比较懵。还好经过几轮服务人员的指点,终于“找到了 北”。海港城里面的商品也不甚便宜,折扣也没有想象中的大,当然这和我们的钱包有限还是有关系的^_^。海港城二层的露台是个理想的观看海景的地方,远眺 远处就是铜锣湾、上环一带的密集楼宇,还有海上穿梭往来的各种TurboJet,很是漂亮。

逛完海港城内部的时候已经是下午5点多了。出了海港城才发现外面有好多顶级品牌专卖店,比如LV、Gucci,店外居然真的有人在排队等待入店。我们不会在这些店里购物,也就看看热闹罢了。

坐TurboJet去澳门

12号早晨,小雨。飞航是11点钟的,我们的时间还是很充裕的,不过心里有事,起的也就早。和LP商量一下,可以早些到中港城,有时间顺便逛下下面的大奥 莱。在中港城解决掉早餐后,向服务人员打探一下在哪里登船。服务人员说10点那班船可能还有空位,可以试试。我们一想早点到澳门也不错。于是就搭上了10 点那趟飞航的“末班车”。喷射飞航里面的情况要比我想象中的豪华,座位也都是软包的,类似飞机上的那种,空间也相对宽敞。TurboJet启动后,颠簸也 不甚明显,没有明显的晕船迹象。外面一直下着小雨,但浪不大,船因此运行的也比较平稳。坐TurboJet喷射飞航一个小时左右就到达了澳门外港客运码 头。

澳门是一个以赌场、旅游为主的旅游城市,入境后从关口出来,外面到处是各家酒店和赌场的免费摆渡车。我们之前就计划好在澳门本岛逛, 氹仔岛就不去了。于是我们乘坐了新葡京的小巴,一路不到10分钟就到了新葡京。下车后第一件事就是将行李寄存在赌场的寄存处。寄存后,到楼上欣赏各类的赌 博游戏。由于LP不是很喜欢这些,转了两圈我们就离开了葡京。

澳门真的不大,从葡京出来沿着“新马路”向西北走,到民政总属大楼后,向右侧的山上走即可到达澳门著名的大三巴牌坊、玫瑰堂、圆炮台等景点。毫无疑问,大 三巴牌坊经典前聚集的游客是最多的。但我个人感觉澳门的教堂是很值得看的,比如说玫瑰圣母堂,不妨进去坐上一小会,体会一下不同宗教信仰的气氛。大三巴牌 坊周围已然成了商业步行街,街道两侧林立着各类店铺,数量最多,人气最旺的莫过于“钜记手信”了,到澳门的游客估计都会从那里买回小食品作为纪念带回国内 的。另外著名的澳门葡式蛋挞也的确名不虚传,味道甚好。澳门元与港币是可以1:1使用的,估计未来一段时间后,澳门元很可能会退出历史舞台。

连续走了多天,我和LP都十分疲劳了,澳门的其他景点实在是逛不动了,于是早早回到葡京取行李,准备回拱北关闸。葡京有免费的摆渡车,但那天等车的人巨多。担心拱北入境人多、过程缓慢,我和LP遂打了一辆Taxi前往拱北,花了50港币,真够贵的。

拱北口岸的人居然并不是特别多,排了一会儿就顺利通关了,而且也没人对我们的包裹进行检查,我们的心也就放下了。

旅行社的车是晚上19:30的,我们等了一个多点,终于登上了回广州的巴士。一路无话,晚上9:30回到广州杨箕地铁站附近,又倒了趟地铁回到动物园的7天连锁。这次的港澳之行算是结束了。

总结

短暂的旅行不足以让我充分认识和体会到当地的风土人情、风俗习惯等,因此说道起来不是那么深刻,遂仅以流水账的方式细致记之,以备忘。

《Understanding and Using C Pointers》要点先睹为快

如果你问十个C程序员:你觉得C语言的核心是什么?这十个程序员都会回答:指针。

指针具备成为C语言核心的两个关键要素:强大争议

* 指针的强大源自于其天生与机器内存模型的适配。使用指针让代码紧凑,并可获得仅次于汇编代码的执行效率;使用指针可以让C程 序员毫不费力地尽情操纵着内存中的每个byte甚至是bit;使用指针可以为C程序员提供无与伦比的操作灵活性。总之,在C语言中指针几乎是无所 不能的代名词。得指针者得天下,没有指针,C语言将变得平庸。

* 成也指针,败也指针。指针的争议之处就在于其在赋予C程序员无比强大的Power的同时,也常常带来无穷的烦恼甚至灾祸,比如 内存问题、调试困难或因指针导致的程序崩溃等。就好比人类社会,做核心人物有争议是难免的,比如足球界有马拉多纳,跳水界有菲尔普斯,斯诺克界有 奥沙利文^_^。

好了,言归正传,我们回到C语言图书上来。目前市面上的C语言书籍,无论国内国外,无论经典还是山寨,基本都是百科大全型,将C语言讲的面面俱 到。比如最近的一本大而全的经典应当属《C Programming , A Modern Approach》,中文版书名为《C语言程序设计:现代方法》第2版。以至于发展到今天,C语言似乎也没啥可讲的了,新出的C语言书大多是与前辈们雷同 的作品。近两年来也有O'reilly出版的C语言书籍,比如:

*《Head First C
*《21st Century C – C Tips from the New School

前者是典型的Head First风格的C教程,后者则是另辟蹊径,结合C语言外延(构建、调试、打包、版本控制、面向对象与C、知名C语言开源库等)进行讲解。这两本书虽形式 有变化,但终究脱离不开百科大全型,针对C的核心-指针并未有较多的深入探讨。而市场上专门写指针的书也稀少的很(似乎鬼子国那边有一本,叫什么 《征服C指针》),唯一的一本书名与指针扯上关系的书《Pointers on C》(中文名“C和指针”)其实依旧是一本C语言大全。于是乎国外著名出版社O'Reilly今年5月出品了一本专门讲解C语言核心 – 指 针的书《Understanding and Using C Pointers》,以满足C程序员深入理解C语言核心并实现进阶的诉求。O'Reilly就是O'Reilly,总是能抓住C语言书籍方面的深度阅读需 求^_^。

《Understanding and Using C Pointers》是个小册子,拢共才200多页,但内容却全部是围绕C语言指针展开的,从最基本的指针声明与操作、C内存模型、动态内存分配,讲到指针 与数组、结构体、字符串的关系,再到最后指针的高级特性:强制转换、Strict Aliasing、线程共享、多态支持等,由浅入深的进行细致的剖析。其作者认为作为C语言核心的指针值得花200页篇幅去讲解,而且期望所有读者在读完 此书后能对C指针有个扎实的理解。总之,这本书对系统C程序员理解C语言的核心-指针是大有裨益的。在其中文版(已经由图灵出版社引进版权了)尚 未出版之前,这里带你先了解以下本书的要点:

第一章 简介

1、指针与内存

   【指针声明语法】
    int *pi;

   【理解复杂指针声明】
    方法:从后向前读,例子:

   const int *pci;

   pci is a variable                                   pci
   pci is a pointer variable                           *pci
   pci is a pointer variable to an integer             int *pci
   pci is a pointer variable to a constant integer     const int *pci

    【地址操作符】
     pi = #

    【输出指针值】
    通过%x、%o、%p输出(printf)指针的值,一般使用%p(%p输出结果不一定等同于%x,是与实现有关的)。例子如下:
     int num = 0;
     int *pi = #
     printf("Address of num: %d Value: %d\n",&num, num);
     printf("Address of pi: %d Value: %d\n",&pi, pi);

     Address of num: 4520836 Value: 0
     Address of pi: 4520824 Value: 4520836

    【通过间接访问操作符解引用指针】
      间接访问操作符*,使用例子如下:
     int num = 5;
     int *pi = #
     printf("%d\n",*pi); // Displays 5
     *pi = 200;
     printf("%d\n",num); // Displays 200

     【指向函数的指针】
        void (*foo)();  // 这个变量声明中的foo就是一个指向函数的指针

     【Null概念】

         null concept
             赋值为NULL的指针变量表示该指针不指向任何内存地址。

         null pointer constant
             null concept的具体支撑实现,其常量值可能是常量值0,也可能不是。依具体实现而定。

         NULL macro
             在许多标准库实现中,NULL定义如下:#define NULL ((void *)0),这也是我们对NULL的通常理解。当然这是依Compiler的具体实现而定的。如果编译 器使用非全0位模式实现了NULL,那该编译器就要保证在指针上下文中使用的NULL或0是null pointer。

         ASCII NUL
             一个全0的字节。

         null string
             一个不包含任何字符的空字符串。C字符串在最后都放置一个结尾0值。

         null statement
             只包含一个分号的空语句。

         指向void的指针
             指向void的指针被成为通用指针,可以用于引用任意类型的数据。它有两个属性:
                    – 指向void的指针与指向char类型的指针具有相同的内存表示与内存对齐约束。
                    – void指针永远不等于其他类型指针,两个赋值为NULL的void pointer是相等的。

             任何指针都可以被赋给一个void pointer,并且之后还可以被转换回其原来的类型。
             int num;
             int *pi = #                   
             void* pv = pi;
             pi = (int*) pv;

            
             void pointer用于数据指针,而不是函数指针。
             全局void pointer或static void pointer在程序启动时被初始化为NULL。

2、指针大小与类型
        在多数现代平台上,指针的大小都是相同的,与其类型无关。指向char的指针与指向结构体的指针大小相同。
        指向函数的指针可能与指向数据类型的指针大小有差异,这要依具体实现而定。
     
     【内存模型】
             在不同机器和编译器下,C语言原生类型的大小是不同的。
             描述不同数据模型的一般记法:I In L Ln LL LLn P Pn,例如LP64、ILP64、LP32等。
 
     【预定义的指针相关类型】
            size_t 用于表示对象的大小的一个安全类型。
            ptrdiff_t 用于处理指针运算
            intptr_t和uintptr_t 用于存 储指针地址

       int num;
       intptr_t *pi = #

3、指针操作符

     【指针运算】
       pointer + integer
           指针实际移动的字节数 = integer + sizeof(integer_type)
           void* pointer的指针运算操作行为是未定义的,依赖Compiler的具体实现。

       pointer – integer
           指针实际移动的字节树 = integer – sizeof(integer_type)。

       pointer1 – pointer2
           两个指针所指地址间的差值,常用于判断数组中元素的先后次序。

       比较pointers

     【指针比较】
              指针可以使用标准的比较操作符(> and <)进行比较,可用来判断数组中元素的先后次序。

4、指针的通常用法
    
     【多级间接寻址】
              双指针(double pointer) – 指向指针的指针。

            char *titles[] = {"A Tale of Two Cities",
                        "Wuthering Heights","Don Quixote",
                        "Odyssey","Moby-Dick","Hamlet",
                        "Gulliver's Travels"};
      char **bestBooks[3];
      bestBooks[0] = &titles[0];
      bestBooks[1] = &titles[3];
      bestBooks[2] = &titles[5];

          
            间接寻址的级数并没有限制,但过多的级数会让人难以理解。
  
    【常量和指针】

            指向常量的指针
         const int limit = 500;
         const int *pci = &limit;

                  *pci = 600;/* Error, 我们不能解引用一个常量指针并修改其所指的内存值 */
                 
                 const int *pci <=> int const *pci;

            指向非常量的常量指针
         int num;
         int *const cpi = &num;

                  *cpi = 25; /* 可以解引用常量指针并修改其所指的内存的值 */
         int limit;
         cpi = &limit; /* Error,我们不能为常量指针重新赋新值 */

         const int limit1 = 300;
         int *const cpi1 = &limit1; /* Warning: 指向非常量的常量指针被用常量 的地址初始化了 */
 
      指向常量的常量指针    
         const int limit = 300;
         const int *const cpci = &limit;
/* 声明后,我们不能通过cpci修改limit,也不能为cpci重新赋值 */

            指向“指向常量的常量指针”的指针
         const int limit = 300;
         const int *const cpci = &limit;
         const int *const *pcpci = &cpci;

第二章 C语言动态内存管理

在运行时通过函数手工从heap分配和释放内存的过程称为动态内存管理。

1、动态内存分配
    【使用malloc函数】
      int *pi = (int*) malloc(sizeof(int));
      *pi = 5;
      free(pi);

    【内存泄漏】
            – 丢失了内存地址
            – 没有调用free函数释放内存

 2、动态分配内存函数
      malloc、realloc、calloc、free
      是否对malloc出的内存起始地址进行强制转型
             int *p = (int*)malloc(4);
             void *pointer可以转换为任意类型指针,没有强制转型也可以。
             但显式的强制转型可以通过代码看出意图,并且与C++编译器(包括早期C编译器)兼容
                       
      你不能用内存分配函数分配的内存去初始化全局或Static变量。
      alloca函数用于在栈上动态分配内存,函数结束时,这块内存自动释放;但alloca不是标准C库函数,移植性差。
      C99支持可变长度数组(VLA),数组声明时的元素个数可以是运行时才能确定值的变量,但数组size一旦在运行时被确定,数组大小就无法再做改变:
       void compute(int size) {
           char* buffer[size];
           …
       }
         

 3、悬挂指针
     被free后依然引用原先内存地址的指针,称为dangling pointer。
     悬挂指针可能导致如下问题:
            – 如果访问其引用的内存,将导致不可预期的结果
            – 如果内存不可访问了,将导致段错误
            – 存在潜在的安全风险。

     悬挂指针引起的问题调试起来十分困难,以下几种方法用于避免发生悬挂指针问题或快速查找悬挂指针问题:
            – free后,设置指针为NULL;
            – 编写一个替代free的函数;
            – 用特定值填充free的内存块,便于快速定位dangling pointer问题
            – 使用第三方工具检查dangling pointer问题

第三章 指针与函数

当与函数一起使用时,指针有两个方面发挥重要作用:
   – 当指针以参数形式传递给函数时,允许函数修改指针所指内存区域的值,并且这种传递方式更加高效;
   – 声明函数指针时,函数的名字被求值为函数的地址。
 
1、程序栈和堆

    【程序栈】
      栈和堆共享一块内存区域。栈在这块区域的低地址部分,堆在高地址部分。
      程序栈用于存放栈帧(stack frame),栈帧中存放的是函数的参数与local变量。
      栈增长方向:向上;堆的增长方向:向下。

    【栈帧的组成】
     一个栈帧包含如下几个元素:
           – 返回地址
           – 本地变量
           – 函数参数
           – 栈指针(Stack pointer)和栈帧指针(base pointer or frame pointer)

     Stack pointer和frame pointer用于运行时系统对栈的管理。前者总是指向栈的顶端;后者指向栈帧内的某个地址,比如函数的返回地址;frame pointer辅助程序访问栈帧内的元素。

     栈帧的创建,见下面例子:
        float average(int *arr, int size) {
            int sum;
            printf("arr: %p\n",&arr);
            printf("size: %p\n",&size);
            printf("sum: %p\n",&sum);

            for(int i=0; i<size; i++) {
                sum += arr[i];
            }
            return (sum * 1.0f) / size;
    }

      average的栈帧中沿着栈“向上”的方向,依次推入的是:
            – 参数 size、arr (与声明的顺序恰好相反)
            – 函数average调用的返回地址
            – 本地变量sum(如果有多个本地变量,推入栈的顺序也与变量声明顺序相反)

      每个线程通常都在自己的栈中创建栈帧。

2、指针作为参数和返回值

      C语言的参数是“按值传递”的,包括指针本身,函数内使用的是参数的copy。
      在处理大数据结构时,将指针作为参数传递给函数或作为返回值会使得程序执行起来更加高效(只是copy一个指针大小的数据,而不是指针所指向的数据对象大 小)。
      另外一个以指针作为函数参数的目的是希望在函数内部对数据进行修改。
      当传递一个指向常量的指针给函数时,其意图为不希望函数内部对指针所指的数据进行修改。例如void passingAddressOfConstants(const int* num1, int* num2),不希望num1所指数据被修改。
      将指针作为返回值返回时,应避免以下几个常见问题:
            – 返回未初始化的指针
            – 返回指向非法地址的指针
            – 返回指向函数本地变量的指针
            – 返回指针后,没有释放其所指的内存块
 
      如果函数要修改的不是参数中指针所指的数据,而是指针本身所指的内存地址,那么应以double pointer形式作为函数参数:

        void allocateArray(int **arr, int size, int value) {
            *arr = (int*)malloc(size * sizeof(int));
            if(*arr != NULL) {
                for(int i=0; i<size; i++) {
                    *(*arr+i) = value;
                }
            }
        }

      int *vector = NULL;
      allocateArray(&vector,5,45);

3、函数指针
      函数指针就是存放函数地址的指针。 
      使用函数指针可能导致程序运行变慢(可能感知不到),因为函数指针的使用可能导致CPU无法正确的运用分支预测,导致CPU流水线中断。

    【声明函数指针】

      函数指针的声明看起来像函数原型,比如:void (*foo)(int i);
      程序员应该确保通过函数指针调用函数的正确使用,因为C编译器不会检查是否正确的为函数指针传入正确的参数(类型、顺序以及个数)。
      通常我们用typedef声明一个函数指针类型,比如:
          typedef void (*funcptr)(int i);
          funcptr fp = foo;

    【函数指针强制转型】
     
      一个类型的函数指针可以被强制转为另外一种类型函数指针。
      转型后的指针 == 转型前的指针
     
        typedef int (*fptrToSingleInt)(int);
        typedef int (*fptrToTwoInts)(int,int);
        int add(int, int);
        fptrToTwoInts fptrFirst = add;
        fptrToSingleInt fptrSecond = (fptrToSingleInt)fptrFirst;
        fptrFirst = (fptrToTwoInts)fptrSecond;
        printf("%d\n",fptrFirst(5,6));

      在函数指针间转换,很可能导致函数调用失败。

第四章 指针与数组

1、数组概述

数组与指针记法关系紧密,在特定上下文中可以相互替换。
数组内部表示中并没有数组长度信息。
 
  【一维数组】
    int vector[5];

    一维数组是一个线性结构。数组下标起始于0,终止于(元素个数-1)。

  【二维数组】
    int matrix[2][3] = {{1,2,3},{4,5,6}};

    二维数组使用行和列标识数组元素。这类数组需要被映射到一个一维地址空间中。
    在C中,二维数组的第一行放在内存的最开始处,接下来是第二行,…,直到最后一行,这就是所谓的“行主序”。

  【多维数组】
    int arr3d[3][2][4] = {
        {{1, 2, 3, 4}, {5, 6, 7, 8}},
        {{9, 10, 11, 12}, {13, 14, 15, 16}},
        {{17, 18, 19, 20}, {21, 22, 23, 24}}
  };

    二维以上的维数的数组称为多维数组,其元素内存分配依旧遵守二维数组那种映射方式。

2、指针记法(notation)与数组

    指针记法与数组记法在一定场合可以互换,但两者并不完全相同。
    数组名单独使用时,我们得到的是数组的地址;该地址等同于数组内第一个元素的地址。

  int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;
  int (*pv)[5] = &vector;

    vector与&vector不同,前者返回指向一个整型变量的指针(int *),后者返回一个指向整个数组的指针(int[5] *)。
  pv[i] <=> *(pv + i)
  *(pv + i) <=> *(vector + i)

  【指针与数组间的不同】

    int vector[5] = {1, 2, 3, 4, 5};
  int *pv = vector;

    sizeof(vector) = 20 != sizeof(pv)

    pv是lvalue,可以被修改而指向不同的地址;比如pv = pv + 1
    而vector不能被修改。vector = vector + 1这个表达式是错误的,不过pv = vector + 1是ok的。

  【使用malloc创建一维数组】
    int *pv = (int*) malloc(5 * sizeof(int));
    pv[3] = 10;

     可使用realloc改变malloc创建的数组的大小。
    
3、传递一维数组
    两种记法:数组记法和指针记法,分别如下:
    void displayArray(int arr[], int size);
    void displayArray(int* arr, int size);

    无论哪种,displayArray函数体内int arr[]或int *arr都将以int *arr方式使用,即数组名退化为指针,sizeof(arr) = 指针长度,而不是数组总长度。

   【一维指针数组】
   
    int* arr[5];
    for(int i=0; i<5; i++) {
        arr[i] = (int*)malloc(sizeof(int));
        *arr[i] = i;
    }

   【指针与多维数组】
         多维数组可以看成是由子数组组成的,就好比二维数组的每行都可以看成是一个一维数组。
         int matrix[2][5] = {{1,2,3,4,5},{6,7,8,9,10}};
         int (*pmatrix)[5] = matrix;

4、传递多维数组

   void display2DArray(int arr[][5], int rows);<=>
   void display2DArray(int (*arr)[5], int rows);

      上面两个版本是等价的。两个版本都指定了列的值,因为编译器需要知道每行的元素个数。

     注意第二个版本不等价于void display2DArray(int *arr[5], int rows)

      在void display2DArrayUnknownSize(int *arr, int rows, int cols)的 函数体实现中,你不能使用arr[i][j],因为arr并未被声明为二维数组。

5、动态分配二维数组

     【采用不连续的内存分配方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int *) malloc(columns * sizeof(int));
    }

     【采用连续内存分配的方式】

    int rows = 2;
    int columns = 5;
    int **matrix = (int **) malloc(rows * sizeof(int *));
    matrix[0] = (int *) malloc(rows * columns * sizeof(int));
    for (int i = 1; i < rows; i++)
        matrix[i] = matrix[0] + i * columns;

       or

    int *matrix = (int *)malloc(rows * columns * sizeof(int));

第五章 指针与字符串

1、字符串基础

     字符串:以ASCII结尾'\0'字符结尾的字符序列。
     分类:字节字符串(byte string) – char类型字符序列
               宽字符串(wide string) – wchar_t 类型字符序列(每个字符16bit or 32bit,依编译器实现而定)
     字符串声明:char header[32] or char *header

    【字符串字面量池(String literal pool)】
      字符串字面量定义后将被放在字面量池中。这块内存区域存放的是组成字符串的字符序列。当一个字面量多次使用时,通常在字面量池中只存储一份该字符串。这将 降低程序的内存使用量。并且通常情况下,字面量池中的字符串是immutable的。

      大多数编译器都提供了编译开关,用于指示是否关闭字符串字面量池,比如Gcc的-fwritable-strings。

     【字符串初始化】、
            char *header = "Media Player";

      or
      char header[] = "Media Player";

      or
      char header[13];
      strcpy(header,"Media Player");

      or
      char *header = (char*) malloc(strlen("Media Player")+1);
      strcpy(header,"Media Player");

2、标准字符串操作

      比较字符串:strcmp
      拷贝字符串:strcpy
      连接字符串:strcat

3、传递字符串

      传递简单字符串:
      size_t stringLength(char* string) ;
      size_t stringLength(char string[]);

      传递字符串常量:
      size_t stringLength(const char* string);

4、返回字符串

         返回一个字面量:return "Boston Processing Center"
         动态分配的内存:
         char* spaces = (char*) malloc(number + 1);
         … …
         return spaces;

         返回local字符串的地址是危险的。

5、函数指针与字符串

第六章 指针与结构体

1、简介

    【如何为结构体分配内存】      
      结构体的大小往往大于该结构体所有字段大小之和,因为有数据对齐的需求,导致编译器在进行结构体内存分配时进行了padding操作。特定数据类型具有一 定的对齐要求,比如short类型的字段要求其地址能被2整除,而integer类型的字段要求其起始地址能被4整除。

      考虑到这些多余分配的内存,你应该谨慎对待如下操作:
      – 小心使用指针运算
      – 结构体数组的元素间有多余内存空间

    【结构体内存释放】
      为结构体分配内存时,运行时不会自动为结构体内的指针字段分配内存;同理,释放结构体内存时,运行时也不会自动释放结构体内指针字段所指向的内存。

    【避免malloc和free的额外开销】
     malloc和free多次重复调用时,会给程序带来额外的开销。一个解决方法就是自己维护一份已分配的结构。需要时,从这个池里取出一份,释放时,直接 返回给池中。如果没有可用的结构时,才考虑新创建一个。

2、使用指针支持数据结构

无论是简单还是复杂的数据结构,指针都提供了更加灵活的支持,包括链表、队列、栈以及树等。

第七章 安全问题以及不当使用指针
   
深入理解指针以及其正确的使用方法有利于开发出安全可信赖的应用。

OS引入了一些提升安全的技术,比如 Address Space Layout Randomization和Data Execution Prevention。

【Address Space Layout Randomization (ASLR) ,地址空间布局随机化】
  ASLR技术使得程序的数据区域随机布局,数据区域包括:代码、栈、堆。随机的放置这些区域让代码攻击行为很难精确预测特定代码的内存地址并使用它们。

【Data Execution Prevention(DEP),数据执行保护】
  DEP技术会阻止执行非执行数据区域中的代码。在一些攻击中,一些非执行数据区域中的数据被恶意覆写为代码,执行权也被转移到那里。但有了DEP后,这些 恶意代码将无法执行。

1、指针声明与初始化

   【不正确的指针声明】
     int* ptr1, ptr2;
      ptr1是指针,但ptr2只是一个整型变量。

      正确声明方法:int *ptr1, *ptr2; /* 更好的做法是每行仅声明一个变量 */

      下面做法存在同样的问题:
   #define PINT int*
   PINT ptr1, ptr2;

      用typedef就没有问题了:
   typedef int* PINT;
   PINT ptr1, ptr2;

   【使用指针前未初始化】
     使用前未做初始化的指针,常称作野指针(wild pointer):

   int *pi;
    …
   printf(“%d\n”,*pi);

    【处理未初始化的指针】
      指针脸上没有写自己是否做过初始化^_^。通常有三种方法用于对付未初始化的指针:
        – 总是将指针初始化为NULL;
        – 使用assert函数
        – 使用第三方工具
       
2、指针使用问题
   
      缓冲区溢出(Buffer overflow)可能由以下原因导致:
      – 访问数组元素的时候没有检查下标值
      – 做数组指针相关运算时不够谨慎
      – 用gets之类的函数从标准输入读取字符串
      – 使用strcpy和strcat不当

     【测试NULL】
       调用malloc后,总是检查返回值是否为NULL。

     【误用解引用操作符】
       int num;
       int *pi;
       *pi = &num

     【悬挂指针】

     【访问数组越界】

       char firstName[8] = "1234567";
       char middleName[8] = "1234567";
       char lastName[8] = "1234567";
       middleName[-2] = 'X';
       middleName[0] = 'X';
       middleName[10] = 'X';

     【错误计算数组大小】
         当将数组作为参数传递给函数时,务必将函数的Size一并传入,这个Size信息将避免数组访问越界。

     【误用sizeof操作符】
        int buffer[20];
        int *pbuffer = buffer;
        for(int i=0; i<sizeof(buffer); i++) {
            *(pbuffer++) = 0;
        }

         sizeof(buffer)=>sizeof(buffer)/sizeof(buffer[0]);

      【总是匹配指针类型】
      【有界指针(bounded pointer)】
      【字符串安全问题】
        对strcpy和strcat使用不当,会导致缓冲区溢出。
        在C11标准中加入了strcat_s和strcpy_s函数,如果发生缓冲区溢出,它们会返回错误。

      【函数指针问题】
       不要将函数赋值给签名不同的函数指针,这很可能将导致未定义行为发生。
      
3、内存释放问题
      【两次free】
      【清除敏感数据】
         一个良好的实践是覆写哪些不再需要的敏感数据。

        char *name = (char*)malloc(…);
        …
        memset(name,0,sizeof(name));
        free(name);

4、使用静态分析工具

      比如Gcc -Wall等。

第八章  其他零碎的知识点

1、指针转型
      指针转型有几个原因:
      – 访问特定目的的地址
      – 分配一个地址代表一个端口
      – 决定机器的endianess

    【访问特定的地址】
      #define VIDEO_BASE 0xB8000
      int *video = (int *) VIDEO_BASE;
      *video = 'A';

    【访问一个端口】
      #define PORT 0xB0000000
      unsigned int volatile * const port = (unsigned int *) PORT;
      *port = 0x0BF4; // write to the port
      value = *port; // read from the port

    【判断机器的endianess】
      int num = 0×12345678;
      char* pc = (char*) &num;
      for (int i = 0; i < 4; i++) {
          printf("%p: %02x \n", pc, (unsigned char) *pc++);
      }

2、Aliasing、Strict Aliasing和restrict关键字

两个指针同时指向一块相同的内存地址,这两个指针被称为aliasing。

     int num = 5;
     int* p1 = &num;
     int* p2 = &num;

aliasing的使用对编译器生成的代码强加了限制。
如果两个指针引用相同位置,每个指针都可以修改这块地址。当编译器生成读写这块内存的代码时,不总是可以通过将值存储在寄存器中这种办法来优化代 码。对每次引用,将强制使用机器级别的低效load和store操作。

Strict Aliasing:另外一种形式的aliasing。strict aliasing不允许不同类型的指针指向同一块内存区域。下面代码:一个指向整型的指针alias了一个指向float类型的指针了,这违反了Strict Aliasing的规则。

    float number = 3.25f;
    unsigned int *ptrValue = (unsigned int *)&number;
    unsigned int result = (*ptrValue & 0×80000000) == 0;

如果仅仅是符号标志和修饰符不同,是不会影响strict aliasing的,下面的语句是符合Strict aliasing规则的:

    int num;
    const int *ptr1 = &num;
    int *ptr2 = &num;
    int volatile ptr3 = &num;

有些场合,相同数据的不同表示是很有用处的,下面一些方法可以避免与Strict aliasing规则冲突:
        – 使用Union: 多个数据类型的联合体可以规避strict aliasing
        – 关闭strict aliasing :利用编译器提供的开关将strict aliasing关闭(不建议这么做哦),
                     比如Gcc提供的一些开关:
                 -fno-strict-aliasing 关闭strict aliasing
                 -fstrict-aliasing 打开strict aliasing
                 -Wstrict-aliasing 针对strict aliasing相关问题给出警告

        – 使用char pointer:char pointer可以alias任何对象。

       【使用Union实现一个值的多种方式表示】
   
        typedef union _conversion {
            float fNum;
            unsigned int uiNum;
        } Conversion;
        int isPositive1(float number) {
            Conversion conversion = { .fNum =number};
            return (conversion.uiNum & 0×80000000) == 0;
        }

           由于没有指针,所以不存在违反Strict aliasing的问题。

       【Strict Aliasing】
         编译器假设多个不同类型的指针不会引用到同一个数据对象,这样在strict aliasing的规则下,编译器才能够实施一些优化。如果假设不成立,那很可能发生意料之外的结果。

         即使是两个拥有相同字段,但名字不同的结构体,其对应的指针也不能引用同一个对象。但通过typedef结构体类型指针与原类型指针可以引用同一个数据对象。

         typedef struct _person {
            char* firstName;
            char* lastName;
            unsigned int age;
        } Person;
        typedef Person Employee;
        Person* person;
        Employee* employee;

       【使用restrict关键字】
         使用restrict关键字,意即告诉编译器这个指针没有被alias,这样编译器将可以进行优化,生成更为高效的代码。通常的优化方法是缓存这个指针。
         不过即便使用了restrict关键字,对编译器来说也只是一个建议,编译器可自行选择是否进行优化。
         建议新代码中都要使用restrict关键字。

        void add(int size, double * restrict arr1, const double * restrict arr2) {
            for (int i = 0; i < size; i++) {
                arr1[i] += arr2[i];
            }
        }

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double vector2[] = {1.1, 2.2, 3.3, 4.4};
        add(4,vector1,vector2);

         以上是add函数的正确用法。

        double vector1[] = {1.1, 2.2, 3.3, 4.4};
        double *vector3 = vector1;
        add(4,vector1,vector3);
        add(4,vector1,vector1);

        这个例子中vector3与vector1指向同一份数据,也许add可以正常工作,但这个函数的调用结果并不那么可靠。

        标准C库中有多个函数使用了restrict关键字,比如void *memcpy(void * restrict s1, const void * restrict s2, size_t n)等。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats