跳到主要內容

PHP學習-LoadXML與網頁格式錯亂

· 閱讀需 3 分鍾

用php寫後端動態生成網頁內容的時候,用到了DOMDocument類的操作。為了減少創建元素和文本節點的代碼(與效率無關),使用了loadXML()方法載入靜態的HTML文本(通過heredoc)。

$xml = new DOMDocument(); $xml->loadXML(<<<_HTML <div class="-article"> <div class="-article-title" onselectstart="return false;"></div> <hr/> <div class="-article-body"></div> <hr/> <div class="-article-extra"></div> <div class="-aborted"></div> <div class="-article-picture"></div> </div> _HTML ); ...... echo xml->saveXML();

沒錯,就是用loadXML()載入HTML,這樣做是因為saveHTML()的時候會輸出完整的HTML文檔(包含html和body元素)而不是我想要的文章部分,而saveXML()則只需要去除首行的文檔聲明即可。

這裡不談這樣的做法好與不好,只說說我遇到的問題。

遇到的問題是,網頁版面亂了。出現了塊級元素的堆疊,就是生成的元素後面的同級元素變成了它的子元素。感覺是標籤沒有閉合導致的,用瀏覽器看了生成的網頁代碼,終於找到了問題所在。

耗子屎在這一行:

<div class="-aborted"></div>

由於一些原因,生成元素中這塊被廢棄了,後面的代碼又很多地方使用了getElementsByTagName方法獲取指定元素,需要靠子元素的位置定位,所以不好直接刪除(可見裝載靜態的html文檔結構並不是個好點子),於是把這個<div>的類名設置為-aborted,然後統一處理。因為是廢棄元素,所以自然也不會為它生成內容了,最後saveXML輸出的文本中,將這個空元素<div class="-aborted"></div>轉化成了<div class="-aborted"/>。在html5中自閉合標籤是有嚴格控制的,只有特定的標籤才允許,因此<div/>並沒有被瀏覽器認為是一個閉合的標籤,然後和後面的div塊混亂了,才導致的這個問題。

解決辦法是,給heredoc中的廢棄元素的內容加個空格,xml封裝器就不會將它當作空元素了。不過最好還是全都動態生成元素,少生么蛾子。或者使用其他更好的辦法。

PHP-防止靜態資源被直接訪問

· 閱讀需 7 分鍾

用PHP寫後端,想要達到用戶登錄後才可以訪問一些圖片和視頻資源的效果,因此要阻止用戶直接輸入資源地址訪問資源。

找了一些資料,自己總結了幾種方法。

1.根據Referer頭——防盜鏈​

瀏覽器在發起HTTP請求時一般都會一同發送Referer頭。Referer頭是用戶跳轉前的頁面,也就是通過哪個頁面發起的請求。通過禁止非法Referer頭的資源請求可以一定程度防止資源被非法訪問。一般這個都是在web服務器軟件上設置而不是後端處理。由於這個方法並不是很靠譜所以沒試過(畢竟請求頭是由請求方控制的),不過應付一般用戶足夠了,特別適合用來防止其他網站掛自己服務器的資源鏈接以轉移服務器負載,不過一般小網站用不到就是了。

2.通過復雜文件名​

給資源文件賦以隨機的文件名,用數據庫記錄,然後定期或不定期更新文件名,用戶訪問頁面時後端php動態的查詢資源文件名。

這個方法還算可以,缺點是頻繁的數據庫連接將會增大服務器負載。如果服務器支持可以試試數據庫持久連接,不過需要注意持久連接的一些坑,不然可能造成連接鎖死之類的問題。

3.隱藏資源——將資源文件放在用戶無法訪問的目錄。

這樣做有兩種方案可以選擇,一是在用戶需要訪問時將資源文件復制到相應位置(可通過創建硬鏈接避免時間和空間浪費),二是將所有對資源的訪問重定位到一個文件,後端統一驗證身份後輸出文件內容。

第一種其實意義不大,因為總是要讓用戶訪問的,那只要有授權用戶在需要資源文件,你就得把資源放在那裡,然後就誰都可以訪問了,再然後發現問題回到方法2了——還是得改文件名。

第二種方法是比較靠譜的方法。比如我將資源訪問重定向到resource.php這個文件,然後驗證身份後根據GET請求參數去找用戶請求的文件,然後用readfile函數讀取並輸出文件就OK。用戶的看到的網頁源代碼將類似這樣:

<img src="http://127.0.0.1/resource.php?path=filename"/>

filename可以是真正的資源相對路徑,因為用戶反正是無法直接訪問的,暴露文件路徑反而可以省去查數據庫的消耗。需要注意的是,如果需要傳遞的文件路徑中包含特殊字符如“/”等需要轉義。可在後端統一由某個接口封裝,生成安全的資源鏈接,類似這樣:

<?php function getURL($path) { return 'http://127.0.0.1/resource.php?path=' . urlencode($path); } ?> <body> <img src="<?php echo getURL('picture/dog.jpg')?>"> </body>

然後resource.php中驗證用戶是否已授權,如果是且資源訪問合法,readfile('/resource_path/' . $_GET['path']),結束。

這應該是目前最靠譜的辦法,不過它也有缺陷。如果請求的資源是視頻這種比較大的文件,瀏覽器會一直等待資源接收完畢才顯示後面的內容,而不是頁面加載完再以流媒體的方式加載視頻資源,因此這個方法無法用於大文件資源。同時,測試發現,css中的url()資源不支持這樣的方式,因此背景圖片之類的資源也無法使用這種方法。

4.大雜儈——結合兩種方法

非常不幸,我要做的東西正好需要請求大量視頻資源,因此採用方法3中的第二類發現瀏覽器一直轉加載視頻,後面的評論等板塊得等視頻下載完成才加載,完全不能忍。於是想了半天,採取了折中的辦法:對於小圖片、少量圖片、文本資源採用方法3第二方案,也就是資源訪問重定向到resource.php統一處理;對於大圖片、視頻資源、大量圖片和css中的資源,則通過臨時硬鏈接的方式。

上面3中已經說了readfile()輸出的方法,下面說說硬鏈接的具體做法。

我們知道,php中有個session的概念,它為一個會話生成一個id,並在客戶端以cookie的形式保存,在服務器端創建一個唯一的session文件,用於保存與相關會話相關的數據,每次用戶請求中包含的cookie便告訴服務器當前會話的相關數據,比如是否已經登錄等。

這裡正利用了php的session機制。

我在網站目錄中創建了一個temp目錄,它包含一個空文件index.html以防止用戶直接訪問目錄看到目錄下的文件列表(也可在服務器端配置禁止對目錄的訪問),因此用戶可以訪問該目錄下的文件而無法得知它包含哪些子目錄或文件,這是前提。

當用戶頁面需要請求一個需要授權訪問的資源時,如果用戶是授權的(通過session機制判斷),後端生成相應資源一個非隨機的硬鏈接。它是這樣的一個硬鏈接:

  1. 它的父目錄是temp/当前会话session ID/
  2. 它的文件名由它的相對路徑通過哈希算法生成,再加上文件後綴

例如,如果我的資源目錄(禁止用戶訪問)是/resource_path,存在資源/resource_path/video/dog.mp4。網站目錄是/(對於用戶),包含temp子目錄。那麼我的頁面應該這樣寫:

<video src="<?php echo '/temp/' . session_id() . '/' . md5('video/dog.mp4') . '.mp4'?>"></video>

之前的getURL函數變成這樣:

function getURL($path, $flow = false) {
//非法资源
if(!file_exists('/resource_path/' . $path)) {
return '';
}
//小文件资源,采用资源重定向方案
if(!$flow) {
return '/resource.php?path=' . urlencode($path);
}
//获取后缀
preg_match('/\.[^\.\/]*$/', $path, $sufix);
$filename = '/temp/'. session_id() . '/' . md5($path) . $sufix[0];
//判断资源链接是否已创建,WWW_ROOT为网站根目录实际路径
if(file_exists(WWW_ROOT . $filename)) {
return $filename;
}
//创建temp/sessionID目录
else if(!file_exists(dirname(WWW_ROOT . $filename))) {
mkdir(dirname(WWW_ROOT . $filename));
}
//创建硬链接。注意Windows不支持PHP的link函数,看你服务器平台
if(stripos(PHP_OS, 'WIN') === false) {
link(HEVER_ROOT . $path, HEVER_ROOT . $filename);
}
else {
system('mklink /H "' . HEVER_ROOT . $filename . '" "' . HEVER_ROOT . $path . '"');
}
return HEVER_HOME . $filename;
}

當然,md5()的參數也可以另外的算,不一定是相對路徑。

這樣做的好處是,當用戶再次請求相同的資源時,只需確保相應的硬鏈接存在即可直接不管了,因此需要這個硬鏈接的文件名是非隨機的避免查詢數據庫。而且這樣多了一層session id阻隔,即使非法用戶知道了生成資源鏈接文件名的規則也無法訪問到資源,因為資源鏈接是在當前會話的session id目錄下的,除非他能猜到某個會話的session id再模擬發送cookie——還不如讓他猜用戶名和密碼呢。

當然這個方法也不是完美的,由於可能會生成大量的硬鏈接和session文件並且廢棄後不會消失,需要定期執行清理腳本來清除過期的鏈接和文件。我的解決方案是,當用戶登錄或登出時觸發一個腳本,它會清除超過1天未訪問過的session文件和對應的temp目錄中的同名名錄(其實不是同名,session文件還有sess_前綴)。這個靈感來自於wordpress的偽cron機制。

需要注意的是,Windows系統不支持php的link函數,因此得用windows的shell。還有一點,清理php session文件時,如果php.ini中未配置session.save_path,在php中用session_save_path()可能獲取不到php默認的session文件存放目錄,因此建議在文件首主動配置session目錄,使用session_save_path(string $path)函數。

注:以上方案均未經過嚴密的測試,請謹慎參考。

求小於一個整數的質數的個數的n個版本

· 閱讀需 6 分鍾

輸入一個整數n,輸出不大於它的質數的個數。

這是一個經典的問題。不管用什麼算法,思路都是嵌套循環,對小於n的自然數判斷是否質數。

代碼不好貼,就截圖了。文末面有源碼鏈接。

基礎版

最笨的辦法就是按順序分別判斷:
//基础版
int primeNum_0(int n)
{
int div;//试除变量
int count = 0;//质数个数
for (int i = 2; i <= n; ++i)
{
for (div = 2; div < i; ++div)
{
if (i % div == 0)//不是质数
break;
}
if (div == i)//质数
++count;
}
return count;
}

測試輸出:

There are 9592 prime numbers in 99999.
Completed with 1429 ms.

升級版

//升级版
int primeNum_1(int n)
{
int div, count = 0;
for (int i = 2; i <= n; ++i)
{
//试除上限为i的平方根取较大整数
int top = floor(sqrt(i)) + 1;
for (div = 2; div < top; ++div)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

這裡對比上面的基礎版,盡管代碼有些變化,但可以看出它們僅有的區別就是當試除i的數div大於√i時,就不再繼續試除,而判定i為質數。

這裡利用了數本身的性質:如果一個數i可以被不小於√i的整數整除,那麼得到的商一定是不大於√i的整數。因此,在試除到√i時便可以判定i是否為質數了。

這樣一步操作,使運算的次數大大減少。

測試輸出:

There are 9676 prime numbers in 99999.
Completed with 24 ms.
There are 665107 prime numbers in 9999999.
Completed with 5680 ms.

對於輸入99999,對比基礎版的超過1秒的運算時間,升級版只用了不到0.1秒。

升級改良版

在升級版的基礎上,還可以改進。

//升级改良版
int primeNum_2(int n)
{
int div, count = 1;//2直接算作质数
for (int i = 3; i <= n; i = i + 2)
{
//因为试除从3开始,2的试除单独提出来
if(i % 2 == 0)
continue;
int top = floor(sqrt(i)) + 1;
for (div = 3; div < top; div = div + 2)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

這個版本相對於升級版又有了一些改動:試除從3開始,每次步進2。因為從循環裡是3開始的,所以對於2的試除單獨放出來(減少避免在循環中增加條件判斷),不影響性能。同時這樣3也無法正常計算了,索性把3也提前算上,count初始化為2。

這裡的原理也很簡單:任何大於2的偶數不可能是質數。為了應用這個原理,有了上面的改動,雖然代碼變得有些畸形,不過速度卻相對提升了接近一倍。

其實這裡還有個bug,當輸入1或者2的時候也會輸出有兩個質數。可以在循環外加上條件判斷,單獨處理,只是這樣代碼會不那麼美觀。事實上這個算法在設計上本身也很不完美,博主還沒有學過算法,很多不規範的地方請諒解。

測試輸出:

There are 9674 prime numbers in 99999.
Completed with 0 ms.
There are 665105 prime numbers in 9999999.
Completed with 2826 ms.

這裡99999的輸入已經可以在1毫秒之內解決了,9999999用了接近3秒,和升級版的5秒多相比有了不錯的提升。

從速度上來看,99999從20多毫秒提升到1毫秒,二十多倍,而9999999卻只是加快了兩倍左右,這或許和CPU的多任務機制有關,相關內容不怎麼熟悉,就不解釋了。因此用執行時間來反應算法速度並不很科學,或許用變量記錄最內層循環執行的次數會更好。

豪華版


//豪华版
int primeNum_3(int n)
{
int count = 1;
int maxSize;//最大存储质数的个数
//申请内存
if(MAX_SIZE < ceil(sqrt(n)))
maxSize = MAX_SIZE;
else
maxSize = (int)(sqrt(n));
int* primeNums = (int*)malloc((maxSize * sizeof(int)));

primeNums[0] = 2;//2先放进去
int size = 1;//当前存储的质数个数
int div, cur, top;
for (int i = 3; i <= n; ++i)
{
top = ceil(sqrt(i));//试除上限
cur = 0;//当前试除数在存储空间的位置
div = primeNums[cur];
while (i % div != 0)
{
if (div >= top)
{
if (size < maxSize)//判断存储空间是否已满
primeNums[size++] = i;//将质数加入存储数组
++count;
break;
}//找到质数

//若已试除到存储空间最后一个数,步进2
if (cur < size - 1)
div = primeNums[++cur];
else
div += 2;
}
}
free(primeNums);
return count;
}

還有一個可以利用的性質,對於正整數i,如果i 可以被div整除(div為小於i且大於2的非質數),那麼i一定存在小於div的質數可以整除i,因為非質數div可以被分為若幹質數的乘積。

而我們判斷一個數是否是質數是從2開始去試除這個數(即使這個判斷被單獨列出),而2是最小的質數,因此要判斷一個大於2的正整數i是否是質數,只需要判斷是否存在一個數k可以整除i,其中k是[2,√i]區間內的質數。

因此,我們可以建立一個質數存儲表。每當判斷出一個數是質數時,就把這個數加入表中。因為我們是從小到大開始判斷,所以這個表也是從小到大的。然後對於一個數i,只需用這個表中不大於√i的質數去試除i,如果最後一不大於√i的質數都無法整除i,那麼i是一個質數。

這個方法比上一種快一些,但消耗的空間也大得多。

測試程序​

int main()
{
int n = 0;
scanf("%d", &n);
if(n < 2)
return 0;

int result, timeCount;//计时

if(n <= 100000)
{
timeCount = clock();
result = primeNum_0(n);
timeCount = clock() - timeCount;
printf("%d 个质数,基础版,%d 毫秒\n", result, timeCount);
}//数值太大基础版耗时太久

timeCount = clock();
result = primeNum_1(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_2(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级改良版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_3(n);
timeCount = clock() - timeCount;
printf("%d 个质数,豪华版,%d 毫秒\n", result, timeCount);

//system("pause");
}

總結

程序中還有一些考慮不周的地方甚至小bug,例如div的步進放在了跳出判斷的前面,這樣導致了奇質數的平方也被判斷為質數了。在代碼文件中修改了一些,可能仍然存在bug。

[轉載]gdb調試利器

· 閱讀需 7 分鍾

轉自:http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html

GDB是一個由GNU開源組織發布的、UNIX/LINUX操作系統下的、基於命令行的、功能強大的程序調試工具。 對於一名Linux下工作的c++程序員,gdb是必不可少的工具;

1. 啟動gdb

對C/C++程序的調試,需要在編譯前就加上-g選項:
$g++ -g hello.cpp -o hello
調試可執行文件:
$gdb <program>
program也就是你的執行文件,一般在當前目錄下。

調試core文件(core是程序非法執行後core dump後產生的文件):

$gdb <program> <core dump file> $gdb program core.11127
調試服務程序:
$gdb <program> <PID> $gdb hello 11127
如果你的程序是一個服務程序,那麼你可以指定這個服務程序運行時的進程ID。gdb會自動attach上去,並調試他。program應該在PATH環境變量中搜索得到。

2. gdb交互命令

啟動gdb後,進入到交互模式,通過以下命令完成對程序的調試;注意高頻使用的命令一般都會有縮寫,熟練使用這些縮寫命令能提高調試的效率;

運行

  • run:簡記為 r ,其作用是運行程序,當遇到斷點後,程序會在斷點處停止運行,等待用戶輸入下一步的命令。
  • continue (簡寫c ):繼續執行,到下一個斷點處(或運行結束)
  • next:(簡寫 n),單步跟蹤程序,當遇到函數調用時,也不進入此函數體;此命令同 step 的主要區別是,step 遇到用戶自定義的函數,將步進到函數中去運行,而 next 則直接調用函數,不會進入到函數體內。
  • step (簡寫s):單步調試如果有函數調用,則進入函數;與命令n不同,n是不進入調用的函數的
  • until:當你厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程序直到退出循環體。
  • until+行號: 運行至某行,不僅僅用來跳出循環
  • finish: 運行程序,直到當前函數完成返回,並打印函數返回時的堆棧地址和返回值及參數值等信息。
  • call 函數(參數):調用程序中可見的函數,並傳遞“參數”,如:call gdb_test(55)
  • quit:簡記為 q ,退出gdb

設置斷點

  • break n (簡寫b n):在第n行處設置斷點
    (可以帶上代碼路徑和代碼名稱: b OAGUPDATE.cpp:578)
  • b fn1 if a>b:條件斷點設置
  • break func(break縮寫為b):在函數func()的入口處設置斷點,如:break cb_button
  • delete 斷點號n:刪除第n個斷點
  • disable 斷點號n:暫停第n個斷點
  • enable 斷點號n:開啟第n個斷點
  • clear 行號n:清除第n行的斷點
  • info b (info breakpoints) :顯示當前程序的斷點設置情況
  • delete breakpoints:清除所有斷點:

查看源代碼

  • list :簡記為 l ,其作用就是列出程序的源代碼,默認每次顯示10行。
  • list 行號:將顯示當前文件以“行號”為中心的前後10行代碼,如:list 12
  • list 函數名:將顯示“函數名”所在函數的源代碼,如:list main
  • list :不帶參數,將接著上一次 list 命令的,輸出下邊的內容。

打印表達式

  • print 表達式:簡記為 p ,其中“表達式”可以是任何當前正在被測試程序的有效表達式,比如當前正在調試C語言的程序,那麼“表達式”可以是任何C語言的有效表達式,包括數字,變量甚至是函數調用。
  • print a:將顯示整數 a 的值
  • print ++a:將把 a 中的值加1,並顯示出來
  • print name:將顯示字符串 name 的值
  • print gdb_test(22):將以整數22作為參數調用 gdb_test() 函數
  • print gdb_test(a):將以變量 a 作為參數調用 gdb_test() 函數
  • display 表達式:在單步運行時將非常有用,使用display命令設置一個表達式後,它將在每次單步進行指令後,緊接著輸出被設置的表達式及值。如: display a
  • watch 表達式:設置一個監視點,一旦被監視的“表達式”的值改變,gdb將強行終止正在被調試的程序。如: watch a
  • whatis :查詢變量或函數
  • info function: 查詢函數
  • 擴展info locals: 顯示當前堆棧頁的所有變量

查詢運行信息

  • where/bt :當前運行的堆棧列表;
  • bt backtrace 顯示當前調用堆棧
  • up/down 改變堆棧顯示的深度
  • set args 參數:指定運行時的參數
  • show args:查看設置好的參數
  • info program: 來查看程序的是否在運行,進程號,被暫停的原因。

分割窗口

  • layout:用於分割窗口,可以一邊查看代碼,一邊測試:
  • layout src:顯示源代碼窗口
  • layout asm:顯示反匯編窗口
  • layout regs:顯示源代碼/反匯編和CPU寄存器窗口
  • layout split:顯示源代碼和反匯編窗口
  • Ctrl + L:刷新窗口

注解

交互模式下直接回車的作用是重復上一指令,對於單步調試非常方便;

3. 更強大的工具

cgdb

cgdb可以看作gdb的界面增強版,用來替代gdb的 gdb -tui。cgdb主要功能是在調試時進行代碼的同步顯示,這無疑增加了調試的方便性,提高了調試效率。界面類似vi,符合unix/linux下開發人員習慣;如果熟悉gdb和vi,幾乎可以立即使用cgdb。

關於宏定義的問題

· 閱讀需 2 分鍾

C語言中的宏定義有時候很方便,有時候也有些不便。宏最重要的性質之一就是,它是在編譯的時候直接替換相應的關鍵字,只是簡單的替換。所以用宏定義表達式時需要額外注意。

今天打算寫一個函數庫,然後裡面的函數都是駝峰命名風格(例如funcMyFunction)的,想同時實現Pascal風格(FuncMyFunction)調用。顯然把代碼復制一遍定義新函數是很不明智的,體積增加,代碼大量重復,維護不便等等。

於是我就打算通過定義宏來實現:

namespace A
{
#define TestFunc testFunc
// ......
}

但問題就來了,宏定義是無視命名空間的。

也就是說,雖然是在名空間A中定義的,但它是全局有效的。如果實際項目中包含了這個頭文件,而這個項目又包含了其他庫的頭文件,萬一有相同名稱的函數就會出錯,因為所有文件中的"TestFunc"都會被替換為testFunc,然後出錯。雖然這個概率不大,但其實也不算太小。

想了半天也沒想到完美的解決方案,不過想到兩個規避的方法。

  1. 單獨用一個頭文件定義這些宏,然後在編譯時靈活決定是否包含它(或者在頭文件中設置條件編譯)。
  2. 給庫中的函數加上前綴,比如mylib_funcMyFunc,這樣再定義宏也能很大程度上規避重復,這條可以與第一條同時進行,應該比較保險了。

這個問題意義不是很大,不過想到了也就一說。

sublime——解決從命令提示符輸入數據的問題

· 閱讀需 1 分鍾

Sublime Text默認的編譯配置會掠過scanf等輸入函數,直接在輸出窗口顯示運行結果,當需要輸入數據時很不方便。其實可以自己修改sublime的編譯配置來達到目的。

假設%DIR%是sublime安裝目錄;

找到%DIR%\Packages\C++.sublime-package文件,復制一份到其它目錄,把文件後綴改為zip,解壓後找到C++ Single File.sublime-build文件,可用記事本打開並復制內容;

選擇sublime菜單->工具(Tools)->編譯系統(Build System)->新編譯系統(New Build System),新建一個編譯系統,把剛剛復制的內容粘貼到新建的文件中;

修改其中的"variants"項下面,"name": "Run"後面的"shell_cmd"項;

假設原來是:

"shell_cmd": "g++ -m32 "${file}" -o "${file_path}/${file_base_name}" && "${file_path}/${file_base_name} ""

則修改為:

"shell_cmd": "g++ -m32 "${file}" -o "${file_path}/${file_base_name}" && start cmd /c "${file_path}/${file_base_name}""

CTRL+S或點擊菜單文件(File)->保存(Save)保存文件,命名為User-C++.sublime-build(或其它)。

大功告成。

Sublime Text——便攜版編譯環境配置

· 閱讀需 3 分鍾

實驗室電腦沒有GCC,於是在U盤裡復制了一份MinGW的C++編譯環境。由於懶得每次配置環境變量,就想把MinGW放到Sublime目錄裡,直接使用。

但sublime的編譯系統默認的工作目錄是目標文件所在的目錄,所以編譯時提示找不到g++命令。由於U盤的盤符經常會改變,所以也無法固定設置。找了很久,終於找到了辦法。

sublime定義了一組特殊變量:

$file_path
The directory of the current file, e.g. C:\Files.
$file
The full path to the current file, e.g.C:\Files\Chapter1.txt.
$file_name
The name portion of the current file, e.g.Chapter1.txt.
$file_extension
The extension portion of the current file, e.g.txt.
$file_base_name
The name-only portion of the current file, e.g.Document.
$folder
The path to the first folder opened in the current project.
$project
The full path to the current project file.
$project_path
The directory of the current project file.
$project_name
The name portion of the current project file.
$project_extension
The extension portion of the current project file.
$project_base_name
The name-only portion of the current project file.
$packages
The full path to thePackagesfolder.
其中,$packages是可以定位到sublime安裝目錄中的 "Data\Packages" 目錄的。

配置方法

假設%DIR%是sublime安裝目錄;

復制MinGW目錄到%DIR%\Data\Packages目錄;

找到%DIR%\Packages\C++.sublime-package文件(注意和上面的Packages目錄不是同一個),復制一份到其它目錄,把文件後綴改為zip,解壓後找到C++ Single File.sublime-build文件,用記事本打開它,復制內容;

選擇sublime菜單->工具(Tools)->編譯系統(Build System)->新編譯系統(New Build System),新建一個編譯系統,把剛剛復制的內容粘貼到新建的文件中;

修改"working_dir" 後面的"${file_path}" 為"${packages}\MinGW\bin"(g++.exe等所在目錄),其他的也可以根據需要自己修改,注意反斜槓要轉義;

CTRL+S或點擊菜單文件(File)->保存(Save)保存文件,命名為User-C++.sublime-build(或其它)。

大功告成。

數組名和指針那點事

· 閱讀需 6 分鍾

今天在查數組名的意義的時候,聯系到指針的知識,對指針有了一種新的理解。

數組是一種特殊的變量,數組名具有雙重含義(初始):

指向數組第一個成員的指針(如果是多維數組則數組成員也為數組);

代表數組本身(其實說成"指向整個數組的指針"更合適)。

一直對第二個含義不太理解,因為感覺上比較抽象,不知道有什麼實際意義。

今天看到一個例子,終於開悟。

int ary[3];
printf("ary = %p\n" "ary + 1 = %p\n", ary, ary + 1);
printf("&ary = %p\n" "&ary + 1 = %p\n", &ary, &ary + 1);

輸出結果:

ary = 0xffb38dc8
ary + 1 = 0xffb38dcc
&ary = 0xffb38dc8
&ary + 1 = 0xffb38dd4

這就比較有意思了,很明顯,ary和&ary都指向了數組的首地址; ary+1相對ary增加了4,一個int型的長度,指向數組第二個成員首地址; 而&ary+1相對&ary則增加了3*4=12,正好是整個數組的大小。

因此數組名稱的第二個含義是只有當數組名前有&時才體現(可能還有其他情況),單獨使用時則相當於一個指向數組成員類型的指針(初始指向第一個成員首地址,另注意數組成員也可以是數組)。

所以對於一個一維數組int ary[3],它的數組名的含義有兩層:

1):
int pAry = ary; //pAry复制了ary的第一层含义
// OR:
// int pAry = &amp;ary[0];

2):
typedef int ARY[3];
ARY *pAry = ary; //pAry复制了ary的第二层含义

以前有一個疑惑。對於二維數組ary2[3][3],ary2,ary2[0]和&ary2[0][0]的值是一樣的,都是整個數組的首地址。但*ary2的值竟然也是一樣的,這是為什麼呢?

先用之前的方法調試看看:

int ary2[3][3];
printf("ary2 = %p\n" "ary2 + 1 = %p\n", ary2, ary2 + 1);
printf("*ary2 = %p\n" "*ary2 + 1 = %p\n", *ary2, *ary2 + 1);

輸出結果:

ary2 = 0xff9fa4a0
ary2 + 1 = 0xff9fa4ac
*ary2 = 0xff9fa4a0
*ary2 + 1 = 0xff9fa4a4

首先ary毫不意外地體現了數組名的第一層含義: 指向數組第一個成員的指針。由於是二維數組,所以數組的第一個成員也是一個數組,它擁有3個整型成員,大小是12字節。所以ary2+1相對ary2增加12。

那麼*ary2中ary2到底體現的哪一層含義呢?*ary2的實際意義又是什麼?

明顯,ary2+1相對ary增加了4,一個整型的大小,所以ary2是相當於ary2[0]的第一層含義的,即指向ary2[0]的第一個成員ary[0][0]。反推回去,如果ary2中ary2是第二層含義(指向整個數組的指針)的話,那ary2應該得到的是整個數組的數據,而不是一個指針值。所以ary2中ary2是體現的第一層含義,即指向數組第一個成員(ary2[0])的指針。

但是這樣解釋新的問題又來了,運算符不是取出指針指向的值嗎?那ary2[0]取出的應該是什麼呢?應該是ary2[0]這個數組!但我們得到的結果呢?是一個int*型的指針!

這個問題折磨了我很久。它看起來並不重要,因為這不影響我們的實際操作,只要知道結論就夠了。但強迫症還是讓我被迫想出一個說服自己的說法:

問題的關鍵還在於開篇對數組名的解讀。數組的名稱可以當成數組本身的指針,也可以當成數組的第一個成員的指針。對於一維數組,這樣的描述看上去沒有問題,因為ary是可以取出數組第一個成員的值的,數組名像指針一樣。但到了二維數組,情況就不一樣了。直接取二維數組名ary2的值取出來的是ary2[0][0]的地址值,對數組名進行運算,*ary2取出來的還是ary2[0[0]]的地址值。

所以認為,編譯器並沒有為數組名在內存中分配空間,而是由編譯器自己做了一個簡單的映射,將數組名的值設為其第一個類型為數組的定義類型的成員的地址(比如int ary[][]的定義類型就是int),然後由編譯器對不同的操作請求對數組名做出不同的解釋。比如,對於三維數組int ary[2][2][2],編譯器把標識符ary[i][i]的值設為ary[i][i][0]的地址值,而對於標識符ary[1],由於ary[1]的第一個成員ary[1][0]不是int型,所以繼續拆分,直到找到第一個int型的成員ary[1][0][0],將ary[1][0]的值置為ary[1][0][0]的地址值。

由此類推,ary, ary[0], ary[0][0]它們的值是一樣的!都是ary[0][0][0]的地址值!不同的只是它們各自管理的內存空間範圍不同而已,並且它們管理的空間起點是相同的,都是ary[0][0][0]的首地址。注意,數組名的值都是直接被編譯器記錄的,而不是寫入內存,所以是不能對數組名賦值的,對數組名的操作其實是對其具有內存空間的成員的操作。這點和指針不一樣,因為指針本身其實是一個變量,它自己本身是有內存空間的,所以多級指針和多維數組變量名有本質區別!指針是一級一級的依次取值,才能得到最裡層的數據,而數組是名依靠映射關系所以直接取得數據。